diff --git a/OpenRA.Mods.CA/Traits/BotModules/BaseBuilderBotModuleCA.cs b/OpenRA.Mods.CA/Traits/BotModules/BaseBuilderBotModuleCA.cs index bcad07bf2e..d7fbd6313d 100644 --- a/OpenRA.Mods.CA/Traits/BotModules/BaseBuilderBotModuleCA.cs +++ b/OpenRA.Mods.CA/Traits/BotModules/BaseBuilderBotModuleCA.cs @@ -165,6 +165,9 @@ public class BaseBuilderBotModuleCAInfo : ConditionalTraitInfo [Desc("Radius in cells around building being considered for sale to scan for units")] public readonly int SellScanRadius = 8; + [Desc("Delay (in ticks) between finding a good resource point to harvest.")] + public readonly int CheckBestResourceLocationInterval = 123; + public override object Create(ActorInitializer init) { return new BaseBuilderBotModuleCA(init.Self, this); } } @@ -192,9 +195,11 @@ public CPos GetRandomBaseCenter() IPathFinder pathFinder; IBotPositionsUpdated[] positionsUpdatedModules; CPos initialBaseCenter; + public CPos? ResourceCenter; readonly Stack> rallyPoints = new(); int assignRallyPointsTicks; + int checkBestResourceLocationTicks; readonly BaseBuilderQueueManagerCA[] builders; // CA: Uses CA queue manager instead of engine version int currentBuilderIndex = 0; @@ -231,6 +236,7 @@ protected override void TraitEnabled(Actor self) { // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. assignRallyPointsTicks = world.LocalRandom.Next(0, Info.AssignRallyPointsInterval); + checkBestResourceLocationTicks = world.LocalRandom.Next(0, Info.CheckBestResourceLocationInterval); var i = 0; @@ -281,6 +287,31 @@ void IBotTick.BotTick(IBot bot) } } + if (--checkBestResourceLocationTicks <= 0 && resourceLayer != null) + { + checkBestResourceLocationTicks = Info.CheckBestResourceLocationInterval; + + Actor bestconyard = null; + var best = int.MinValue; + + foreach (var conyard in constructionYardBuildings.Actors.Where(a => !a.IsDead)) + { + if (!world.Map.FindTilesInAnnulus(conyard.Location, Info.MinBaseRadius, Info.MaxBaseRadius).Any(a => resourceLayer.GetResource(a).Type != null)) + continue; + + var suitable = -world.FindActorsInCircle(conyard.CenterPosition, WDist.FromCells(Info.MaxBaseRadius)) + .Count(a => (a.Owner.IsAlliedWith(player) && Info.RefineryTypes.Contains(a.Info.Name)) || a.Owner.RelationshipWith(player) == PlayerRelationship.Enemy); + + if (suitable > best) + { + best = suitable; + bestconyard = conyard; + } + } + + ResourceCenter = bestconyard?.Location; + } + BuildingsBeingProduced.Clear(); // PERF: We tick only one type of valid queue at a time diff --git a/OpenRA.Mods.CA/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManagerCA.cs b/OpenRA.Mods.CA/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManagerCA.cs index b686ff624f..2817bf3718 100644 --- a/OpenRA.Mods.CA/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManagerCA.cs +++ b/OpenRA.Mods.CA/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManagerCA.cs @@ -526,13 +526,16 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue) // Try and place the refinery near a resource field if (resourceLayer != null) { - var nearbyResources = world.Map.FindTilesInAnnulus(baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius) + // If we have failed to place to the best refinery point, try and place it near the base center + var resourceCenter = failCount > 0 ? baseCenter : (baseBuilder.ResourceCenter ?? baseCenter); + + var nearbyResources = world.Map.FindTilesInAnnulus(resourceCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius) .Where(a => resourceLayer.GetResource(a).Type != null) .Shuffle(world.LocalRandom).Take(baseBuilder.Info.MaxResourceCellsToCheck); foreach (var r in nearbyResources) { - var found = findPos(baseCenter, r, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius); + var found = findPos(resourceCenter, r, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius); if (found != null) return found; } diff --git a/OpenRA.Mods.CA/Traits/BotModules/MCVManagerBotModuleCA.cs b/OpenRA.Mods.CA/Traits/BotModules/MCVManagerBotModuleCA.cs index c08b7da02e..1ce291472f 100644 --- a/OpenRA.Mods.CA/Traits/BotModules/MCVManagerBotModuleCA.cs +++ b/OpenRA.Mods.CA/Traits/BotModules/MCVManagerBotModuleCA.cs @@ -1,5 +1,5 @@ #region Copyright & License Information -/** +/* * Copyright (c) The OpenRA Combined Arms Developers (see CREDITS). * This file is part of OpenRA Combined Arms, which is free software. * It is made available to you under the terms of the GNU General Public License @@ -18,17 +18,19 @@ namespace OpenRA.Mods.CA.Traits { - [Desc("Manages AI MCVs.")] + public enum BotMcvExpansionMode { CheckResourceCreator, CheckResource, CheckBase } + + [Desc("Manages AI MCVs and expansion.")] public class McvManagerBotModuleCAInfo : ConditionalTraitInfo { [Desc("Actor types that are considered MCVs (deploy into base builders).")] - public readonly HashSet McvTypes = new HashSet(); + public readonly HashSet McvTypes = new(); [Desc("Actor types that are considered construction yards (base builders).")] - public readonly HashSet ConstructionYardTypes = new HashSet(); + public readonly HashSet ConstructionYardTypes = new(); [Desc("Actor types that are able to produce MCVs.")] - public readonly HashSet McvFactoryTypes = new HashSet(); + public readonly HashSet McvFactoryTypes = new(); [Desc("Try to maintain at least this many ConstructionYardTypes, build an MCV if number is below this.")] public readonly int MinimumConstructionYardCount = 1; @@ -36,65 +38,127 @@ public class McvManagerBotModuleCAInfo : ConditionalTraitInfo [Desc("Delay (in ticks) between looking for and giving out orders to new MCVs.")] public readonly int ScanForNewMcvInterval = 20; - [Desc("Minimum distance in cells from center of the base when checking for MCV deployment location.")] - public readonly int MinBaseRadius = 2; + [Desc("Delay (in ticks) between check and build a MCV.")] + public readonly int BuildMcvInterval = 101; - [Desc("Maximum distance in cells from center of the base when checking for MCV deployment location.", - "Only applies if RestrictMCVDeploymentFallbackToBase is enabled and there's at least one construction yard.")] - public readonly int MaxBaseRadius = 20; + [Desc("Move a conyard if have more than 1 conyard, for better expansion ")] + public readonly int MoveConyardTick = 4500; - [Desc("Distance in cells from center of the base when checking near by enemies for MCV deployment location.")] - public readonly int EnemyScanRadius = 8; + [Desc("Tells the AI what building types are considered production.")] + public readonly HashSet ProductionTypes = new(); - [Desc("Should deployment of additional MCVs be restricted to MaxBaseRadius if explicit deploy locations are missing or occupied?")] - public readonly bool RestrictMCVDeploymentFallbackToBase = true; + [Desc("Tells the AI what building types are considered refineries.")] + public readonly HashSet RefineryTypes = new(); - public override object Create(ActorInitializer init) { return new McvManagerBotModuleCA(init.Self, this); } - } + [Desc("Move a conyard even when this is the only conyard, only works for once ")] + public readonly int[] FirstMoveConyardTicks = { -1 }; - public class McvManagerBotModuleCA : ConditionalTrait, IBotTick, IBotPositionsUpdated, IGameSaveTraitData - { - public CPos GetRandomBaseCenter(bool distanceToBaseIsImportant) - { - if (distanceToBaseIsImportant == false) - return initialBaseCenter; + [Desc("Initial expasion mode choosed by AI.")] + public readonly BotMcvExpansionMode InitialExpansionMode = BotMcvExpansionMode.CheckResourceCreator; - var ti = world.Map.Rules.TerrainInfo; - var resourceTypeIndices = new BitArray(ti.TerrainTypes.Length); + /* those are options shared by two or more modes */ + [Desc("Tick in update an indice of resource map.")] + public readonly int UpdateResourceMapInverval = 83; - foreach (var i in world.Map.Rules.Actors["world"].TraitInfos()) - foreach (var t in i.ResourceTypes) - resourceTypeIndices.Set(ti.GetTerrainIndex(t.Value.TerrainType), true); + [Desc("Distance in cells of indice of resource map.")] + public readonly int ResourceMapStride = 18; - var randomConstructionYard = world.Actors.Where(a => a.Owner == player && - Info.ConstructionYardTypes.Contains(a.Info.Name)) - .RandomOrDefault(world.LocalRandom); + /* those are CheckResourceCreator mode options */ + [Desc("Minimum distance in cells around the resource creator location when checking for MCV deployment location.")] + public readonly int CRCmodeMinDeployRadius = 2; - var newResources = world.Map.FindTilesInAnnulus(randomConstructionYard.Location, Info.MaxBaseRadius, world.Map.Grid.MaximumTileSearchRange) - .Where(a => resourceTypeIndices.Get(world.Map.GetTerrainIndex(a))) - .Shuffle(world.LocalRandom).FirstOrDefault(); + [Desc("Maximum distance in cells around the resource creator location when checking for MCV deployment location.")] + public readonly int CRCmodeMaxDeployRadius = 12; - return newResources; - } + [Desc("Tells the AI what types are considered resource creator.")] + public readonly HashSet ResourceCreatorTypes = new(); + + [Desc("Distance in cells to a friendly conyard that AI dislike when choose a expanding location.")] + public readonly int CRCmodeConyardUnfavorRange = 18; + + [Desc("Distance in cells to a friendly refinery that AI dislike when choose a expanding location.")] + public readonly int CRCmodeRefineryUnfavorRange = 12; + + [Desc("Distance in cells that AI try to maintain to the expanding location in deployment.")] + public readonly int CRCmodeTryMaintainRange = 8; + + [Desc("Distance in cells from center of the base when checking near by enemies for MCV expanding location.")] + public readonly int CRCmodeEnemyScanRadius = 10; + + /* those are CheckResource mode options */ + [Desc("Minimum distance in cells from the found resource creator location when checking for MCV deployment location.")] + public readonly int CRmodeMinDeployRadius = 2; + + [Desc("Maximum distance in cells the found resource creator location when checking for MCV deployment location.")] + public readonly int CRmodeMaxDeployRadius = 20; + + [Desc("Distance in cells that AI try to maintain to the expanding location in deployment.")] + public readonly int CRmodeTryMaintainRange = 8; + + [Desc("Resource types that are considered can be harvested.")] + public readonly HashSet ValidResourceTypes = new(); + + /* those are CheckBase mode options */ + [Desc("Minimum distance in cells from center of the base expansion when checking for MCV deployment location.")] + public readonly int CBmodeMinDeployRadius = 2; + + [Desc("Maximum distance in cells from center of the base expansion when checking for MCV deployment location.")] + public readonly int CBmodeMaxDeployRadius = 20; + + public override object Create(ActorInitializer init) { return new McvManagerBotModuleCA(init.Self, this); } + } + + public class McvManagerBotModuleCA : ConditionalTrait, IBotTick, IBotRespondToAttack + { + const int PositiveMaxFailedAttempts = 3; + const int NegativeMaxFailedAttempts = 1; readonly World world; readonly Player player; - - readonly Predicate unitCannotBeOrdered; + readonly ActorIndex.OwnerAndNamesAndTrait mcvs; + readonly ActorIndex.OwnerAndNamesAndTrait constructionYards; + readonly ActorIndex.OwnerAndNamesAndTrait mcvFactories; + readonly int resourceIndiceStride; IBotPositionsUpdated[] notifyPositionsUpdated; IBotRequestUnitProduction[] requestUnitProduction; - CPos initialBaseCenter; int scanInterval; + int buildMCVInterval; + int moveConyardInterval; + int updateResourceMapInterval; bool firstTick = true; + bool firstUndeploy = true; + bool allowfallback; + + BotMcvExpansionMode mcvExpansionMode; + int mcvDeploymentMinDeployRadius; + int mcvDeploymentMaxDeployRadius; + int mcvDeploymentTryMaintainRange; + + int maxFailedAttempts = 3; + int failedAttempts; + CPos? lastFailedExpandSpot; + + int attackrespondcooldown = 20; + + int pathDistanceSquareFactor; + + BitArray resourceTypeIndices = null; + (CPos Center, int Value)[] resourceMapIndices = null; + int resourceMapIndicesColumnCount; + int resourceMapIndicesRowCount; + int updateResourceMapIndex; public McvManagerBotModuleCA(Actor self, McvManagerBotModuleCAInfo info) : base(info) { world = self.World; player = self.Owner; - unitCannotBeOrdered = a => a.Owner != player || a.IsDead || !a.IsInWorld; + mcvs = new ActorIndex.OwnerAndNamesAndTrait(world, info.McvTypes, player); + constructionYards = new ActorIndex.OwnerAndNamesAndTrait(world, info.ConstructionYardTypes, player); + mcvFactories = new ActorIndex.OwnerAndNamesAndTrait(world, info.McvFactoryTypes, player); + resourceIndiceStride = info.ResourceMapStride; } protected override void Created(Actor self) @@ -105,25 +169,391 @@ protected override void Created(Actor self) // for bot modules always to the Player actor. notifyPositionsUpdated = self.TraitsImplementing().ToArray(); requestUnitProduction = self.TraitsImplementing().ToArray(); + moveConyardInterval = Info.FirstMoveConyardTicks.RandomOrDefault(world.LocalRandom); + if (moveConyardInterval == -1) + firstUndeploy = false; } protected override void TraitEnabled(Actor self) { // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. - scanInterval = world.LocalRandom.Next(Info.ScanForNewMcvInterval, Info.ScanForNewMcvInterval * 2); + scanInterval = world.LocalRandom.Next(Info.ScanForNewMcvInterval, Info.ScanForNewMcvInterval << 1); + buildMCVInterval = world.LocalRandom.Next(Info.BuildMcvInterval, Info.BuildMcvInterval << 1); + updateResourceMapInterval = world.LocalRandom.Next(Info.UpdateResourceMapInverval, Info.UpdateResourceMapInverval << 1); + + var map = world.Map; + var ti = map.Rules.TerrainInfo; + + if (resourceTypeIndices == null) + { + resourceTypeIndices = new BitArray(ti.TerrainTypes.Length); + foreach (var i in world.Map.Rules.Actors["world"].TraitInfos()) + { + foreach (var t in i.ResourceTypes) + { + if (Info.ValidResourceTypes.Contains(t.Value.TerrainType)) + resourceTypeIndices.Set(ti.GetTerrainIndex(t.Value.TerrainType), true); + } + } + } + + if (resourceMapIndices == null) + { + resourceMapIndicesColumnCount = (map.MapSize.X + resourceIndiceStride - 1) / resourceIndiceStride; + resourceMapIndicesRowCount = (map.MapSize.Y + resourceIndiceStride - 1) / resourceIndiceStride; + resourceMapIndices = Exts.MakeArray(resourceMapIndicesColumnCount * resourceMapIndicesRowCount, i => (new MPos( + i % resourceMapIndicesColumnCount * resourceIndiceStride + (resourceIndiceStride >> 1), + i / resourceMapIndicesRowCount * resourceIndiceStride + (resourceIndiceStride >> 1)).ToCPos(map), 0)).Shuffle(world.LocalRandom).ToArray(); + for (var i = 0; i < resourceMapIndices.Length; i++) + UpdateResourceMap(i); + + pathDistanceSquareFactor = resourceMapIndicesColumnCount * resourceMapIndicesColumnCount + resourceMapIndicesRowCount * resourceMapIndicesRowCount; + } + } + + void SwitchExpansionMode(BotMcvExpansionMode nextMode) + { + mcvExpansionMode = nextMode; + switch (nextMode) + { + case BotMcvExpansionMode.CheckResourceCreator: + mcvDeploymentMinDeployRadius = Info.CRmodeMinDeployRadius; + mcvDeploymentMaxDeployRadius = Info.CRmodeMaxDeployRadius; + mcvDeploymentTryMaintainRange = Info.CRCmodeTryMaintainRange; + break; + + case BotMcvExpansionMode.CheckResource: + mcvDeploymentMinDeployRadius = Info.CBmodeMinDeployRadius; + mcvDeploymentMaxDeployRadius = Info.CBmodeMaxDeployRadius; + mcvDeploymentTryMaintainRange = Info.CRmodeTryMaintainRange; + break; + + case BotMcvExpansionMode.CheckBase: + mcvDeploymentMinDeployRadius = Info.CBmodeMinDeployRadius; + mcvDeploymentMaxDeployRadius = Info.CBmodeMaxDeployRadius; + mcvDeploymentTryMaintainRange = (Info.CBmodeMaxDeployRadius + Info.CBmodeMinDeployRadius) >> 1; + break; + + default: + break; + } + } + + void FindBadDeploySpot(CPos? failedSpot) + { + lastFailedExpandSpot = failedSpot.Value; + if (++failedAttempts >= maxFailedAttempts) + { + failedAttempts = 0; + switch (mcvExpansionMode) + { + case BotMcvExpansionMode.CheckResourceCreator: + SwitchExpansionMode(BotMcvExpansionMode.CheckResource); + break; + + case BotMcvExpansionMode.CheckResource: + SwitchExpansionMode(BotMcvExpansionMode.CheckBase); + break; + + case BotMcvExpansionMode.CheckBase: + SwitchExpansionMode(BotMcvExpansionMode.CheckResourceCreator); + maxFailedAttempts = NegativeMaxFailedAttempts; + break; + } + } } - void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) + void FindGoodDeploySpot() { - initialBaseCenter = newLocation; + lastFailedExpandSpot = null; + if (--failedAttempts <= -maxFailedAttempts) + { + maxFailedAttempts = PositiveMaxFailedAttempts; + switch (mcvExpansionMode) + { + case BotMcvExpansionMode.CheckResourceCreator: + failedAttempts = -maxFailedAttempts; + break; + + case BotMcvExpansionMode.CheckBase: + failedAttempts = maxFailedAttempts - 1; + SwitchExpansionMode(BotMcvExpansionMode.CheckResource); + break; + + case BotMcvExpansionMode.CheckResource: + failedAttempts = maxFailedAttempts - 1; + SwitchExpansionMode(BotMcvExpansionMode.CheckResourceCreator); + break; + } + } } - void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) { } + public (CPos? ExpandLocation, int Attraction) GetExpandCenter(CPos mcv_loc, bool allowfallback) + { + /* + * The following codes find expansion point as the important reference for current MCV's deployment: + * 1. For CheckBase and CheckResource modes, resourceMapIndices is used to find the best indice for the expansion point. + * 2. For CheckResourceCreator mode, all resource creator locations are considered as candidates, and the best one is chosen as the expansion point. + * + * indiceStrideSquare (which is equal to resourceIndiceStride * resourceIndiceStride) is used as the basic unit to calculate the attraction of a candidate, + * so we can compare the attraction on the same scale on different factors, such as candidate's distance to current MCV and ally construction yard & refinery within range, etc: + * + * 1). the weight of candidate's distance-square to current MCV: range from 0 to -indiceStrideSquare. + * + * The reason why: + * + * It is calculated as "(candidate - mcv_loc).LengthSquared / pathDistanceSquareFactor". + * noted that: pathDistanceSquareFactor = resourceMapIndicesColumnCount * resourceMapIndicesColumnCount + resourceMapIndicesRowCount * resourceMapIndicesRowCount, + * + * Consider a map, we divide it at the length of resourceIndiceStride = r, and then its resourceMapIndicesColumnCount = a, resourceMapIndicesRowCount = b, + * so the map.width ≈ a*r, map.height ≈ b*r, + * the maximum distance-square between two points on the map is (a*r)(a*r) + (b*r)(b*r), + * so the maximum "weight of candidate's distance to current MCV" is from 0 to -((a*r)(a*r) + (b*r)(b*r)) / (a*a + b*b) = -r*r = -indiceStrideSquare. + * + * 2). the weight of friendly construction yard within range: -indiceStrideSquare. If it belongs to an ally, -indiceStrideSquare/2. + * + * 3). the weight of enemy building within range: -indiceStrideSquare*4. + * + * 4). the weight of friendly refinery within range (not for CheckBase mode): -indiceStrideSquare. If it belongs to an ally, -indiceStrideSquare/2. + * + * 5). the weight of resource amount (only for CheckResource mode): from 0 to +indiceStrideSquare/2. + * + * The reason why: + * + * The maximum resource amount in a stride of resource map is proximately indiceStrideSquare (full of it), but an stride full of resource is less likely + * have room for buildings. so the we perfer only the half of the stride full of resource the most, which may give us enough room to place buildings. + * + * so the weight can be: (indiceStrideSquare/4) - |(strideValue - (resourceIndiceStride/2)) * resourceIndiceStride/2| + * + */ + var indiceStrideSquare = resourceIndiceStride * resourceIndiceStride; + switch (mcvExpansionMode) + { + /* + * CheckBase mode only considers the distance to current MCV, ally construction yard within range and enemy buildings within range. + * Attaction has a base value of indiceStrideSquare >> 3 (1/8 of the maximum distance weight, 1/(2*sqrt(2))≈ 1/2.8 of the maximum distance in map) + */ + case BotMcvExpansionMode.CheckBase: + var cb_conyardlocs = world.ActorsHavingTrait().Where(a => a.Owner.IsAlliedWith(player) + && Info.ConstructionYardTypes.Contains(a.Info.Name)).Select(a => (a.Location, a.Owner == player)).ToArray(); + + CPos? cb_suitablespot = null; + var cb_best = int.MinValue; + + foreach (var (center, value) in resourceMapIndices) + { + var attraction = indiceStrideSquare >> 3; + + attraction -= (center - mcv_loc).LengthSquared / pathDistanceSquareFactor; + + if (lastFailedExpandSpot == center) + continue; + + if (world.FindActorsInCircle(world.Map.CenterOfCell(center), WDist.FromCells(resourceIndiceStride)).Any(a => !a.Disposed + && (player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy) + && a.Info.HasTraitInfo() + && a.Info.HasTraitInfo())) + attraction -= indiceStrideSquare << 2; + + foreach (var (location, isAlly) in cb_conyardlocs) + { + var sdistance = (center - location).LengthSquared; + if (sdistance <= indiceStrideSquare) + { + if (isAlly) + attraction -= indiceStrideSquare; + else + attraction -= indiceStrideSquare << 1; + } + } + + if (!allowfallback) + { + var sdistance = (center - mcv_loc).LengthSquared; + if (sdistance <= indiceStrideSquare) + attraction -= indiceStrideSquare << 1; + } + + if (attraction > cb_best) + { + cb_best = attraction; + cb_suitablespot = center; + } + } + + return (cb_suitablespot ?? mcv_loc, cb_best); + + /* + * CheckResourceCreator mode considers the distance to current MCV, ally construction yard & refinery within range, + * Attaction has a base value of indiceStrideSquare >> 4 (1/16 of the maximum distance weight, 1/4 maximum map distance in map). + * Because resource amount is also a factor to be considered compared to other modes, so the base attaction is reduced by half. + */ + case BotMcvExpansionMode.CheckResource: + + var cr_refinarylocs = world.ActorsHavingTrait().Where(a => a.Owner == player && Info.RefineryTypes.Contains(a.Info.Name)) + .Select(a => (a.Location, a.Owner != player)) + .ToArray(); + + var cr_conyardlocs = world.ActorsHavingTrait().Where(a => a.Owner.IsAlliedWith(player) + && Info.ConstructionYardTypes.Contains(a.Info.Name)).Select(a => (a.Location, a.Owner != player)).ToArray(); + + CPos? cr_suitablespot = null; + var cr_best = int.MinValue; + + foreach (var (center, value) in resourceMapIndices) + { + // don't have to look into cell with no resource + if (value == 0) + continue; + + var attraction = indiceStrideSquare >> 4; + + // it is better that resource cells takes only half of the indice cells, which give us the place to place building. + attraction += (indiceStrideSquare >> 2) - (Math.Abs(value - (resourceIndiceStride >> 1)) * resourceIndiceStride >> 1); + + attraction -= (center - mcv_loc).LengthSquared / pathDistanceSquareFactor; + + if (world.FindActorsInCircle(world.Map.CenterOfCell(center), WDist.FromCells(resourceIndiceStride)).Any(a => !a.Disposed + && (player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy) + && a.Info.HasTraitInfo() + && a.Info.HasTraitInfo())) + attraction -= indiceStrideSquare; + + foreach (var (location, isAlly) in cr_refinarylocs) + { + var sdistance = (center - location).LengthSquared; + if (sdistance <= indiceStrideSquare) + { + if (isAlly) + attraction -= indiceStrideSquare; + else + attraction -= indiceStrideSquare << 1; + } + } + + foreach (var (location, isAlly) in cr_conyardlocs) + { + var sdistance = (center - location).LengthSquared; + if (sdistance <= indiceStrideSquare) + { + if (isAlly) + attraction -= indiceStrideSquare; + else + attraction -= indiceStrideSquare << 1; + } + } + + if (!allowfallback) + { + var sdistance = (center - mcv_loc).LengthSquared; + if (sdistance <= indiceStrideSquare) + attraction -= indiceStrideSquare << 1; + } + + if (attraction > cr_best) + { + cr_best = attraction; + cr_suitablespot = center; + } + } + + var resourceloc = world.Map.FindTilesInAnnulus(cr_suitablespot.Value, mcvDeploymentMaxDeployRadius, world.Map.Grid.MaximumTileSearchRange) + .Where(a => a != lastFailedExpandSpot && resourceTypeIndices.Get(world.Map.GetTerrainIndex(a))).RandomOrDefault(world.LocalRandom); + + return (resourceloc, cr_best); + + /* + * CheckResourceCreator mode considers the distance to current MCV, ally construction yard & refinery within range, + * Attaction has a base value of indiceStrideSquare >> 3 (1/8 of the maximum distance weight, 1/(2*sqrt(2))≈ 1/2.8 of the maximum distance in map) + */ + case BotMcvExpansionMode.CheckResourceCreator: + var crc_conyardlocs = world.ActorsHavingTrait().Where(a => a.Owner.IsAlliedWith(player) + && Info.ConstructionYardTypes.Contains(a.Info.Name)).Select(a => (a.Location, a.Owner != player)).ToArray(); + + var crc_refinarylocs = world.ActorsHavingTrait().Where(a => a.Owner.IsAlliedWith(player) && Info.RefineryTypes.Contains(a.Info.Name)) + .Select(a => (a.Location, a.Owner != player)) + .ToArray(); + + var crc_rescreators = world.ActorsHavingTrait().Where(a => Info.ResourceCreatorTypes.Contains(a.Info.Name)); + + CPos? crc_suitablelocation = null; + var crc_best = int.MinValue; + + foreach (var rescreator in crc_rescreators) + { + var attraction = indiceStrideSquare >> 3; + + attraction -= (rescreator.Location - mcv_loc).LengthSquared / pathDistanceSquareFactor; + + if (lastFailedExpandSpot == rescreator.Location) + continue; + + if (world.FindActorsInCircle(rescreator.CenterPosition, WDist.FromCells(Info.CRCmodeEnemyScanRadius)).Any(a => !a.Disposed + && (player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy) + && a.Info.HasTraitInfo() + && a.Info.HasTraitInfo())) + attraction -= indiceStrideSquare << 2; + + foreach (var (location, isAlly) in crc_refinarylocs) + { + var sdistance = (rescreator.Location - location).LengthSquared; + if (sdistance <= Info.CRCmodeRefineryUnfavorRange * Info.CRCmodeRefineryUnfavorRange) + { + if (isAlly) + attraction -= indiceStrideSquare; + else + attraction -= indiceStrideSquare << 1; + } + } + + foreach (var (location, isAlly) in crc_conyardlocs) + { + var sdistance = (rescreator.Location - location).LengthSquared; + if (sdistance <= Info.CRCmodeConyardUnfavorRange * Info.CRCmodeConyardUnfavorRange) + { + if (isAlly) + attraction -= indiceStrideSquare; + else + attraction -= indiceStrideSquare << 1; + } + } + + if (!allowfallback) + { + var sdistance = (rescreator.Location - mcv_loc).LengthSquared; + if (sdistance <= indiceStrideSquare) + attraction -= indiceStrideSquare << 1; + } + + if (attraction > crc_best) + { + crc_best = attraction; + crc_suitablelocation = rescreator.Location; + } + } + + return (crc_suitablelocation, crc_best); + + default: + return (null, int.MinValue); + } + } void IBotTick.BotTick(IBot bot) { + attackrespondcooldown--; + if (firstTick) { + // check which mode we should use in map + SwitchExpansionMode(Info.InitialExpansionMode); + if (mcvExpansionMode != BotMcvExpansionMode.CheckBase && !world.ActorsHavingTrait().Any()) + { + SwitchExpansionMode(BotMcvExpansionMode.CheckResource); + if (!world.Map.AllCells.Any(a => resourceTypeIndices.Get(world.Map.GetTerrainIndex(a)))) + SwitchExpansionMode(BotMcvExpansionMode.CheckBase); + } + DeployMcvs(bot, false); firstTick = false; } @@ -132,31 +562,54 @@ void IBotTick.BotTick(IBot bot) { scanInterval = Info.ScanForNewMcvInterval; DeployMcvs(bot, true); + } - // No construction yards - Build a new MCV - if (ShouldBuildMCV()) - { - var unitBuilder = requestUnitProduction.FirstOrDefault(Exts.IsTraitEnabled); - if (unitBuilder != null) - { - var mcvInfo = AIUtils.GetInfoByCommonName(Info.McvTypes, player); - if (unitBuilder.RequestedProductionCount(bot, mcvInfo.Name) == 0) - unitBuilder.RequestUnitProduction(bot, mcvInfo.Name); - } - } + if (--buildMCVInterval <= 0) + { + buildMCVInterval = Info.BuildMcvInterval; + BuildMCV(bot); + } + + if (--updateResourceMapInterval <= 0) + { + updateResourceMapInterval = Info.UpdateResourceMapInverval; + UpdateResourceMap(updateResourceMapIndex); + updateResourceMapIndex = (updateResourceMapIndex + 1) % resourceMapIndices.Length; + } + + if (--moveConyardInterval <= 0) + { + moveConyardInterval = Info.MoveConyardTick; + UnDeployConyard(bot); } } - bool ShouldBuildMCV() + void UpdateResourceMap(int index) { - // Only build MCV if we don't already have one in the field. - var allowedToBuildMCV = AIUtils.CountActorByCommonName(Info.McvTypes, player) == 0; - if (!allowedToBuildMCV) - return false; - - // Build MCV if we don't have the desired number of construction yards, unless we have no factory (can't build it). - return AIUtils.CountBuildingByCommonName(Info.ConstructionYardTypes, player) < Info.MinimumConstructionYardCount && - AIUtils.CountBuildingByCommonName(Info.McvFactoryTypes, player) > 0; + resourceMapIndices[index] = (resourceMapIndices[index].Center, world.Map.FindTilesInAnnulus(resourceMapIndices[index].Center, 0, resourceIndiceStride) + .Count(a => resourceTypeIndices.Get(world.Map.GetTerrainIndex(a)))); + } + + void BuildMCV(IBot bot) + { + var conyardNum = AIUtils.CountActorByCommonName(constructionYards); + var mcvNum = AIUtils.CountActorByCommonName(mcvs); + + // Only build MCV if we have no mcv in the field (make it an exception if have no conyard), + // don't have one in production and don't have the desired number of construction yards + if ((conyardNum <= 0 && mcvNum > 1) || (conyardNum > 0 && mcvNum > 0) + || conyardNum + mcvNum >= Info.MinimumConstructionYardCount || AIUtils.CountActorByCommonName(mcvFactories) <= 0 + || mcvFactories.Actors.Any(a => !a.IsDead && a.TraitsImplementing().Any(t => t.Enabled && t.AllQueued() + .Any(q => Info.McvTypes.Contains(q.Item)))) || player.PlayerActor.TraitsImplementing().Any(t => t.Enabled && t.AllQueued() + .Any(q => Info.McvTypes.Contains(q.Item))) || Info.McvTypes.Count <= 0) + return; + + var unitBuilder = requestUnitProduction.FirstEnabledTraitOrDefault(); + if (unitBuilder == null) + return; + var mcvType = Info.McvTypes.Random(world.LocalRandom); + if (unitBuilder.RequestedProductionCount(bot, mcvType) <= 0) + unitBuilder.RequestUnitProduction(bot, mcvType); } void DeployMcvs(IBot bot, bool chooseLocation) @@ -168,35 +621,65 @@ void DeployMcvs(IBot bot, bool chooseLocation) DeployMcv(bot, mcv, chooseLocation); } + void UnDeployConyard(IBot bot) + { + if (firstUndeploy) + { + if (world.ActorsHavingTrait().Count(a => a.Owner == player && Info.ProductionTypes.Contains(a.Info.Name)) >= 2 + && world.ActorsHavingTrait().Any(a => a.Owner == player && Info.RefineryTypes.Contains(a.Info.Name))) + { + var idleconyards = constructionYards.Actors.Where(a => !a.IsDead).ToList(); + + if (idleconyards.Count > 0) + { + bot.QueueOrder(new Order("DeployTransform", idleconyards[0], true)); + allowfallback = false; + } + } + + firstUndeploy = false; + moveConyardInterval = world.LocalRandom.Next(Info.MoveConyardTick, Info.MoveConyardTick * 2); + } + else + { + var idleconyards = constructionYards.Actors + .Where(a => !a.IsDead && !a.TraitsImplementing() + .Any(t => t.Enabled && t.AllQueued().Any(q => Info.RefineryTypes.Contains(q.Item)))) + .ToList(); + + if (idleconyards.Count > 1) + bot.QueueOrder(new Order("DeployTransform", idleconyards[0], true)); + } + } + // Find any MCV and deploy them at a sensible location. void DeployMcv(IBot bot, Actor mcv, bool move) { if (move) { - // If we lack a base, we need to make sure we don't restrict deployment of the MCV to the base! - var restrictToBase = AIUtils.CountBuildingByCommonName(Info.ConstructionYardTypes, player) > 0; - var transformsInfo = mcv.Info.TraitInfo(); - var desiredLocation = ChooseMcvDeployLocation(transformsInfo.IntoActor, transformsInfo.Offset, restrictToBase); + var desiredLocation = ChooseMcvDeployLocation(mcv, transformsInfo.IntoActor, transformsInfo.Offset, allowfallback); if (desiredLocation == null) return; bot.QueueOrder(new Order("Move", mcv, Target.FromCell(world, desiredLocation.Value), true)); - } - // If the MCV has to move first, we can't be sure it reaches the destination alive, so we only - // update base and defense center if the MCV is deployed immediately (i.e. at game start). - // TODO: This could be adressed via INotifyTransform. - foreach (var n in notifyPositionsUpdated) - { - n.UpdatedBaseCenter(mcv.Location); - n.UpdatedDefenseCenter(mcv.Location); + allowfallback = true; + + if (constructionYards.Actors.Any(a => !a.IsDead)) + { + foreach (var n in notifyPositionsUpdated) + { + n.UpdatedBaseCenter(desiredLocation.Value); + n.UpdatedDefenseCenter(desiredLocation.Value); + } + } } bot.QueueOrder(new Order("DeployTransform", mcv, true)); } - CPos? ChooseMcvDeployLocation(string actorType, CVec offset, bool distanceToBaseIsImportant) + CPos? ChooseMcvDeployLocation(Actor mcv, string actorType, CVec offset, bool allowfallback) { var actorInfo = world.Map.Rules.Actors[actorType]; var bi = actorInfo.TraitInfoOrDefault(); @@ -204,71 +687,82 @@ void DeployMcv(IBot bot, Actor mcv, bool move) return null; // Find the buildable cell that is closest to pos and centered around center - Func findPos = (center, target, minRange, maxRange) => + CPos? FindDeployCell(CPos? centerCell, CPos? targetCell, int minRange, int maxRange, int tryMaintainRange) { - var cells = world.Map.FindTilesInAnnulus(center, minRange, maxRange); + if (!centerCell.HasValue || !targetCell.HasValue) + return null; + + var target = targetCell.Value; + var center = centerCell.Value; + + var cells = world.Map.FindTilesInAnnulus(target, minRange, maxRange); - // Sort by distance to target if we have one + /* First, sort the cells that keep tryMaintainRange to target (meanwhile direction is from center to target) the first to be considered + * by using following code. The idea is to use a linear combination of two distances-square for sorting weight. + * + * See comments in https://github.com/OpenRA/OpenRA/pull/22035 for explaination. + */ if (center != target) - cells = cells.OrderBy(c => (c - target).LengthSquared); + { + var theta = tryMaintainRange; + var deta = (target - center).Length - tryMaintainRange; + cells = cells.OrderBy(c => deta * (c - target).LengthSquared + theta * (c - center).LengthSquared); + } else cells = cells.Shuffle(world.LocalRandom); + CPos? bestcell = null; foreach (var cell in cells) + { if (world.CanPlaceBuilding(cell + offset, actorInfo, bi, null)) - return cell; - - return null; - }; - - var baseCenter = GetRandomBaseCenter(distanceToBaseIsImportant); - - CPos? bc = findPos(baseCenter, baseCenter, Info.MinBaseRadius, - distanceToBaseIsImportant ? Info.MaxBaseRadius : world.Map.Grid.MaximumTileSearchRange); - - if (!bc.HasValue) - return null; + { + bestcell = cell; + break; + } + } - baseCenter = bc.Value; + if (bestcell == null) + return null; - var wPos = world.Map.CenterOfCell(bc.Value); - var newBaseRadius = new WDist(Info.EnemyScanRadius * 1024); + // If the best cell is not ideal ( >= tryMaintainRange + 2), which means there might be some huge blockers + // so we fall back to default behavior, which is the directly closest cell to target + if (center != target && (bestcell.Value - target).LengthSquared >= (tryMaintainRange + 2) * (tryMaintainRange + 2)) + { + cells = cells.OrderBy(c => (c - target).LengthSquared); + foreach (var cell in cells) + { + if (world.CanPlaceBuilding(cell + offset, actorInfo, bi, null)) + return (cell - target).LengthSquared < (bestcell.Value - target).LengthSquared ? cell : bestcell; + } + } - var enemies = world.FindActorsInCircle(wPos, newBaseRadius) - .Where(a => !a.Disposed && player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy && a.Info.HasTraitInfo()); + return bestcell; + } - if (enemies.Count() > 0) - return null; + var (expandCenter, attraction) = GetExpandCenter(mcv.Location, allowfallback); - var self = world.FindActorsInCircle(wPos, newBaseRadius) - .Where(a => !a.Disposed && a.Owner == player - && (Info.McvTypes.Contains(a.Info.Name) || (Info.ConstructionYardTypes.Contains(a.Info.Name) && a.Info.HasTraitInfo()))); + var bc = FindDeployCell(mcv.Location, expandCenter, mcvDeploymentMinDeployRadius, mcvDeploymentMaxDeployRadius, mcvDeploymentTryMaintainRange); - if (self.Count() > 0) - return null; + if (bc.HasValue && attraction > 0) + FindGoodDeploySpot(); + else + FindBadDeploySpot(expandCenter); - return baseCenter; + return bc.Value; } - List IGameSaveTraitData.IssueTraitData(Actor self) + void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e) { - if (IsTraitDisabled) - return null; - - return new List() + if (attackrespondcooldown <= 0 && Info.McvTypes.Contains(self.Info.Name)) { - new MiniYamlNode("InitialBaseCenter", FieldSaver.FormatValue(initialBaseCenter)) - }; - } - - void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data) - { - if (self.World.IsReplay) - return; - - var initialBaseCenterNode = data.NodeWithKeyOrDefault("InitialBaseCenter"); - if (initialBaseCenterNode != null) - initialBaseCenter = FieldLoader.GetValue("InitialBaseCenter", initialBaseCenterNode.Value.Value); + attackrespondcooldown = 20; + bot.QueueOrder(new Order("DeployTransform", self, false)); + if (AIUtils.CountActorByCommonName(constructionYards) == 0) + { + foreach (var n in notifyPositionsUpdated) + n.UpdatedBaseCenter(self.Location); + } + } } } } diff --git a/mods/ca/rules/ai.yaml b/mods/ca/rules/ai.yaml index 80745becf3..669c090cbf 100644 --- a/mods/ca/rules/ai.yaml +++ b/mods/ca/rules/ai.yaml @@ -808,22 +808,33 @@ Player: McvTypes: mcv, amcv, smcv ConstructionYardTypes: fact, afac, sfac McvFactoryTypes: weap, weap.td, airs, wsph + ProductionTypes: barr,tent,hand,pyle,port,weap,airs,weap.td,wsph + RefineryTypes: proc,proc.td,proc.scrin + FirstMoveConyardTicks: -1, 4200, 4200, 2850, 2850 ScanForNewMcvInterval: 80 - MaxBaseRadius: 25 MinimumConstructionYardCount: 3 + ResourceCreatorTypes: mine, gmine, split2, split3, splitblue + ValidResourceTypes: Ore, Gems, Tiberium, BlueTiberium, BlackTiberium McvManagerBotModuleCA@upper: RequiresCondition: enable-vhard-ai || enable-hard-ai McvTypes: mcv, amcv, smcv ConstructionYardTypes: fact, afac, sfac McvFactoryTypes: weap, weap.td, airs, wsph + ProductionTypes: barr,tent,hand,pyle,port,weap,airs,weap.td,wsph + RefineryTypes: proc,proc.td,proc.scrin ScanForNewMcvInterval: 80 - MaxBaseRadius: 25 MinimumConstructionYardCount: 2 + ResourceCreatorTypes: mine, gmine, split2, split3, splitblue + ValidResourceTypes: Ore, Gems, Tiberium, BlueTiberium, BlackTiberium McvManagerBotModuleCA@lower: RequiresCondition: enable-normal-ai || enable-easy-ai || enable-naval-ai McvTypes: mcv, amcv, smcv ConstructionYardTypes: fact, afac, sfac + ProductionTypes: barr,tent,hand,pyle,port,weap,airs,weap.td,wsph + RefineryTypes: proc,proc.td,proc.scrin McvFactoryTypes: weap, weap.td, airs, wsph + ResourceCreatorTypes: mine, gmine, split2, split3, splitblue + ValidResourceTypes: Ore, Gems, Tiberium, BlueTiberium, BlackTiberium BaseBuilderBotModuleCA@brutal-vhard: RequiresCondition: enable-brutal-ai || enable-vhard-ai BuildingQueues: BuildingSQ, BuildingMQ, Upgrade