From e026a0f00f1a655f52a84ebadc29f1f5165391e0 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Wed, 13 Jan 2016 21:45:15 +0000 Subject: [PATCH 1/4] Create proper data structures for hardcoded AI classes. --- OpenRA.Mods.Common/AI/BaseBuilder.cs | 19 +++---- OpenRA.Mods.Common/AI/HackyAI.cs | 78 ++++++++++++++++------------ 2 files changed, 52 insertions(+), 45 deletions(-) diff --git a/OpenRA.Mods.Common/AI/BaseBuilder.cs b/OpenRA.Mods.Common/AI/BaseBuilder.cs index ae7e359276..ba4341c7c7 100644 --- a/OpenRA.Mods.Common/AI/BaseBuilder.cs +++ b/OpenRA.Mods.Common/AI/BaseBuilder.cs @@ -170,12 +170,8 @@ namespace OpenRA.Mods.Common.AI return true; } - ActorInfo GetProducibleBuilding(string commonName, IEnumerable buildables, Func orderBy = null) + ActorInfo GetProducibleBuilding(HashSet actors, IEnumerable buildables, Func orderBy = null) { - HashSet actors; - if (!ai.Info.BuildingCommonNames.TryGetValue(commonName, out actors)) - return null; - var available = buildables.Where(actor => { // Are we able to build this? @@ -205,7 +201,7 @@ namespace OpenRA.Mods.Common.AI var buildableThings = queue.BuildableItems(); // This gets used quite a bit, so let's cache it here - var power = GetProducibleBuilding("Power", buildableThings, + var power = GetProducibleBuilding(ai.Info.BuildingCommonNames.Power, buildableThings, a => a.TraitInfos().Where(i => i.UpgradeMinEnabledLevel < 1).Sum(p => p.Amount)); // First priority is to get out of a low power situation @@ -221,7 +217,7 @@ namespace OpenRA.Mods.Common.AI // Next is to build up a strong economy if (!ai.HasAdequateProc() || !ai.HasMinimumProc()) { - var refinery = GetProducibleBuilding("Refinery", buildableThings); + var refinery = GetProducibleBuilding(ai.Info.BuildingCommonNames.Refinery, buildableThings); if (refinery != null && HasSufficientPowerForActor(refinery)) { HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (refinery)", queue.Actor.Owner, refinery.Name); @@ -238,7 +234,7 @@ namespace OpenRA.Mods.Common.AI // Make sure that we can spend as fast as we are earning if (ai.Info.NewProductionCashThreshold > 0 && playerResources.Resources > ai.Info.NewProductionCashThreshold) { - var production = GetProducibleBuilding("Production", buildableThings); + var production = GetProducibleBuilding(ai.Info.BuildingCommonNames.Production, buildableThings); if (production != null && HasSufficientPowerForActor(production)) { HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (production)", queue.Actor.Owner, production.Name); @@ -257,7 +253,7 @@ namespace OpenRA.Mods.Common.AI && playerResources.Resources > ai.Info.NewProductionCashThreshold && ai.CloseEnoughToWater()) { - var navalproduction = GetProducibleBuilding("NavalProduction", buildableThings); + var navalproduction = GetProducibleBuilding(ai.Info.BuildingCommonNames.NavalProduction, buildableThings); if (navalproduction != null && HasSufficientPowerForActor(navalproduction)) { HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (navalproduction)", queue.Actor.Owner, navalproduction.Name); @@ -274,7 +270,7 @@ namespace OpenRA.Mods.Common.AI // Create some head room for resource storage if we really need it if (playerResources.Resources > 0.8 * playerResources.ResourceCapacity) { - var silo = GetProducibleBuilding("Silo", buildableThings); + var silo = GetProducibleBuilding(ai.Info.BuildingCommonNames.Silo, buildableThings); if (silo != null && HasSufficientPowerForActor(silo)) { HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (silo)", queue.Actor.Owner, silo.Name); @@ -308,8 +304,7 @@ namespace OpenRA.Mods.Common.AI // If we're considering to build a naval structure, check whether there is enough water inside the base perimeter // and any structure providing buildable area close enough to that water. // TODO: Extend this check to cover any naval structure, not just production. - if (ai.Info.BuildingCommonNames.ContainsKey("NavalProduction") - && ai.Info.BuildingCommonNames["NavalProduction"].Contains(name) + if (ai.Info.BuildingCommonNames.NavalProduction.Contains(name) && (waterState == Water.NotEnoughWater || !ai.CloseEnoughToWater())) continue; diff --git a/OpenRA.Mods.Common/AI/HackyAI.cs b/OpenRA.Mods.Common/AI/HackyAI.cs index 3730d2307f..bc3331e274 100644 --- a/OpenRA.Mods.Common/AI/HackyAI.cs +++ b/OpenRA.Mods.Common/AI/HackyAI.cs @@ -23,6 +23,23 @@ namespace OpenRA.Mods.Common.AI { public sealed class HackyAIInfo : IBotInfo, ITraitInfo { + public class UnitCategories + { + public readonly HashSet Mcv = new HashSet(); + } + + public class BuildingCategories + { + public readonly HashSet ConstructionYard = new HashSet(); + public readonly HashSet VehiclesFactory = new HashSet(); + public readonly HashSet Refinery = new HashSet(); + public readonly HashSet Power = new HashSet(); + public readonly HashSet Barracks = new HashSet(); + public readonly HashSet Production = new HashSet(); + public readonly HashSet NavalProduction = new HashSet(); + public readonly HashSet Silo = new HashSet(); + } + [Desc("Ingame name this bot uses.")] public readonly string Name = "Unnamed Bot"; @@ -133,11 +150,13 @@ namespace OpenRA.Mods.Common.AI public readonly Dictionary BuildingFractions = null; [Desc("Tells the AI what unit types fall under the same common name. Only supported entry is Mcv.")] - public readonly Dictionary> UnitsCommonNames = null; + [FieldLoader.LoadUsing("LoadUnitCategories", true)] + public readonly UnitCategories UnitsCommonNames; [Desc("Tells the AI what building types fall under the same common name.", "Possible keys are ConstructionYard, Power, Refinery, Silo , Barracks, Production, VehiclesFactory, NavalProduction.")] - public readonly Dictionary> BuildingCommonNames = null; + [FieldLoader.LoadUsing("LoadBuildingCategories", true)] + public readonly BuildingCategories BuildingCommonNames; [Desc("What buildings should the AI have a maximum limit to build.")] public readonly Dictionary BuildingLimits = null; @@ -147,6 +166,18 @@ namespace OpenRA.Mods.Common.AI [FieldLoader.LoadUsing("LoadDecisions")] public readonly List PowerDecisions = new List(); + static object LoadUnitCategories(MiniYaml yaml) + { + var categories = yaml.Nodes.First(n => n.Key == "UnitsCommonNames"); + return FieldLoader.Load(categories.Value); + } + + static object LoadBuildingCategories(MiniYaml yaml) + { + var categories = yaml.Nodes.First(n => n.Key == "BuildingCommonNames"); + return FieldLoader.Load(categories.Value); + } + static object LoadDecisions(MiniYaml yaml) { var ret = new List(); @@ -378,57 +409,38 @@ namespace OpenRA.Mods.Common.AI return World.ActorsHavingTrait().Count(a => a.Owner == owner && a.Info.Name == unit); } - int? CountBuildingByCommonName(string commonName, Player owner) + int CountBuildingByCommonName(HashSet buildings, Player owner) { - if (!Info.BuildingCommonNames.ContainsKey(commonName)) - return null; - return World.ActorsHavingTrait() - .Count(a => a.Owner == owner && Info.BuildingCommonNames[commonName].Contains(a.Info.Name)); + .Count(a => a.Owner == owner && buildings.Contains(a.Info.Name)); } - public ActorInfo GetBuildingInfoByCommonName(string commonName, Player owner) + public ActorInfo GetInfoByCommonName(HashSet names, Player owner) { - if (commonName == "ConstructionYard") - return Map.Rules.Actors.Where(k => Info.BuildingCommonNames[commonName].Contains(k.Key)).Random(Random).Value; - - return GetInfoByCommonName(Info.BuildingCommonNames, commonName, owner); - } - - public ActorInfo GetUnitInfoByCommonName(string commonName, Player owner) - { - return GetInfoByCommonName(Info.UnitsCommonNames, commonName, owner); - } - - public ActorInfo GetInfoByCommonName(Dictionary> names, string commonName, Player owner) - { - if (!names.Any() || !names.ContainsKey(commonName)) - throw new InvalidOperationException("Can't find {0} in the HackyAI UnitsCommonNames definition.".F(commonName)); - - return Map.Rules.Actors.Where(k => names[commonName].Contains(k.Key)).Random(Random).Value; + return Map.Rules.Actors.Where(k => names.Contains(k.Key)).Random(Random).Value; } public bool HasAdequateFact() { // Require at least one construction yard, unless we have no vehicles factory (can't build it). - return CountBuildingByCommonName("ConstructionYard", Player) > 0 || - CountBuildingByCommonName("VehiclesFactory", Player) == 0; + return CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) > 0 || + CountBuildingByCommonName(Info.BuildingCommonNames.VehiclesFactory, Player) == 0; } public bool HasAdequateProc() { // Require at least one refinery, unless we have no power (can't build it). - return CountBuildingByCommonName("Refinery", Player) > 0 || - CountBuildingByCommonName("Power", Player) == 0; + return CountBuildingByCommonName(Info.BuildingCommonNames.Refinery, Player) > 0 || + CountBuildingByCommonName(Info.BuildingCommonNames.Power, Player) == 0; } public bool HasMinimumProc() { // Require at least two refineries, unless we have no power (can't build it) // or barracks (higher priority?) - return CountBuildingByCommonName("Refinery", Player) >= 2 || - CountBuildingByCommonName("Power", Player) == 0 || - CountBuildingByCommonName("Barracks", Player) == 0; + return CountBuildingByCommonName(Info.BuildingCommonNames.Refinery, Player) >= 2 || + CountBuildingByCommonName(Info.BuildingCommonNames.Power, Player) == 0 || + CountBuildingByCommonName(Info.BuildingCommonNames.Barracks, Player) == 0; } // For mods like RA (number of building must match the number of aircraft) @@ -1014,7 +1026,7 @@ namespace OpenRA.Mods.Common.AI // No construction yards - Build a new MCV if (!HasAdequateFact() && !self.World.ActorsHavingTrait() .Any(a => a.Owner == Player && a.Info.HasTraitInfo())) - BuildUnit("Vehicle", GetUnitInfoByCommonName("Mcv", Player).Name); + BuildUnit("Vehicle", GetInfoByCommonName(Info.UnitsCommonNames.Mcv, Player).Name); foreach (var q in Info.UnitQueues) BuildUnit(q, unitsHangingAroundTheBase.Count < Info.IdleBaseUnitsMaximum); From d4815407f2323f4324b3dcc593f7412fd6d08647 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Wed, 13 Jan 2016 22:11:32 +0000 Subject: [PATCH 2/4] Remove hardcoded trait assumptions from MCV and ConYards. --- OpenRA.Mods.Common/AI/HackyAI.cs | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/OpenRA.Mods.Common/AI/HackyAI.cs b/OpenRA.Mods.Common/AI/HackyAI.cs index bc3331e274..8ad5e6245b 100644 --- a/OpenRA.Mods.Common/AI/HackyAI.cs +++ b/OpenRA.Mods.Common/AI/HackyAI.cs @@ -202,11 +202,11 @@ namespace OpenRA.Mods.Common.AI public CPos GetRandomBaseCenter() { - var randomBaseBuilding = World.ActorsHavingTrait() - .Where(a => a.Owner == Player && !a.Info.HasTraitInfo()) + var randomConstructionYard = World.Actors.Where(a => a.Owner == Player && + Info.BuildingCommonNames.ConstructionYard.Contains(a.Info.Name)) .RandomOrDefault(Random); - return randomBaseBuilding != null ? randomBaseBuilding.Location : initialBaseCenter; + return randomConstructionYard != null ? randomConstructionYard.Location : initialBaseCenter; } public bool IsEnabled; @@ -609,8 +609,8 @@ namespace OpenRA.Mods.Common.AI List FindEnemyConstructionYards() { - return World.ActorsHavingTrait() - .Where(a => Player.Stances[a.Owner] == Stance.Enemy && !a.IsDead && !a.Info.HasTraitInfo()).ToList(); + return World.Actors.Where(a => Player.Stances[a.Owner] == Stance.Enemy && !a.IsDead && + Info.BuildingCommonNames.ConstructionYard.Contains(a.Info.Name)).ToList(); } void CleanSquads() @@ -721,7 +721,7 @@ namespace OpenRA.Mods.Common.AI void FindNewUnits(Actor self) { var newUnits = self.World.ActorsHavingTrait() - .Where(a => a.Owner == Player && !a.Info.HasTraitInfo() && !activeUnits.Contains(a)); + .Where(a => a.Owner == Player && !Info.UnitsCommonNames.Mcv.Contains(a.Info.Name) && !activeUnits.Contains(a)); foreach (var a in newUnits) { @@ -845,17 +845,14 @@ namespace OpenRA.Mods.Common.AI void InitializeBase(Actor self) { // Find and deploy our mcv - var mcv = self.World.ActorsHavingTrait().FirstOrDefault(a => a.Owner == Player); + var mcv = self.World.Actors.FirstOrDefault(a => a.Owner == Player && + Info.UnitsCommonNames.Mcv.Contains(a.Info.Name)); if (mcv != null) { initialBaseCenter = mcv.Location; defenseCenter = mcv.Location; - - // Don't transform the mcv if it is a fact - // HACK: This needs to query against MCVs directly - if (mcv.Info.HasTraitInfo()) - QueueOrder(new Order("DeployTransform", mcv, false)); + QueueOrder(new Order("DeployTransform", mcv, false)); } else BotDebug("AI: Can't find BaseBuildUnit."); @@ -865,9 +862,8 @@ namespace OpenRA.Mods.Common.AI // backup location within the main base. void FindAndDeployBackupMcv(Actor self) { - // HACK: This needs to query against MCVs directly - var mcvs = self.World.ActorsHavingTrait() - .Where(a => a.Owner == Player && a.Info.HasTraitInfo()); + var mcvs = self.World.Actors.Where(a => a.Owner == Player && + Info.UnitsCommonNames.Mcv.Contains(a.Info.Name)); foreach (var mcv in mcvs) { @@ -1024,8 +1020,8 @@ namespace OpenRA.Mods.Common.AI return; // No construction yards - Build a new MCV - if (!HasAdequateFact() && !self.World.ActorsHavingTrait() - .Any(a => a.Owner == Player && a.Info.HasTraitInfo())) + if (!HasAdequateFact() && !self.World.Actors.Any(a => a.Owner == Player && + Info.UnitsCommonNames.Mcv.Contains(a.Info.Name))) BuildUnit("Vehicle", GetInfoByCommonName(Info.UnitsCommonNames.Mcv, Player).Name); foreach (var q in Info.UnitQueues) From bec059a3c74a103d4daf38e01b44b763152e66ff Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Wed, 13 Jan 2016 22:13:11 +0000 Subject: [PATCH 3/4] Remove unnecessary assumption about non-mobile. --- OpenRA.Mods.Common/AI/HackyAI.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenRA.Mods.Common/AI/HackyAI.cs b/OpenRA.Mods.Common/AI/HackyAI.cs index 8ad5e6245b..ad734408b0 100644 --- a/OpenRA.Mods.Common/AI/HackyAI.cs +++ b/OpenRA.Mods.Common/AI/HackyAI.cs @@ -318,7 +318,7 @@ namespace OpenRA.Mods.Common.AI public bool EnoughWaterToBuildNaval() { var baseProviders = World.ActorsHavingTrait() - .Where(a => a.Owner == Player && !a.Info.HasTraitInfo()); + .Where(a => a.Owner == Player); foreach (var b in baseProviders) { @@ -343,7 +343,7 @@ namespace OpenRA.Mods.Common.AI public bool CloseEnoughToWater() { var areaProviders = World.ActorsHavingTrait() - .Where(a => a.Owner == Player && !a.Info.HasTraitInfo()); + .Where(a => a.Owner == Player); foreach (var a in areaProviders) { From bee77db1e3aaed32f8a27bcdf12dd4127994eebf Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Wed, 13 Jan 2016 22:13:27 +0000 Subject: [PATCH 4/4] Rename variables for clarity. --- OpenRA.Mods.Common/AI/HackyAI.cs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/OpenRA.Mods.Common/AI/HackyAI.cs b/OpenRA.Mods.Common/AI/HackyAI.cs index ad734408b0..d46be16375 100644 --- a/OpenRA.Mods.Common/AI/HackyAI.cs +++ b/OpenRA.Mods.Common/AI/HackyAI.cs @@ -669,19 +669,19 @@ namespace OpenRA.Mods.Common.AI FindAndDeployBackupMcv(self); } - CPos FindNextResource(Actor self) + CPos FindNextResource(Actor harvester) { - var harvInfo = self.Info.TraitInfo(); - var mobileInfo = self.Info.TraitInfo(); + var harvInfo = harvester.Info.TraitInfo(); + var mobileInfo = harvester.Info.TraitInfo(); var passable = (uint)mobileInfo.GetMovementClass(World.TileSet); var path = pathfinder.FindPath( - PathSearch.Search(World, mobileInfo, self, true, - loc => domainIndex.IsPassable(self.Location, loc, passable) && self.CanHarvestAt(loc, resLayer, harvInfo, territory)) + PathSearch.Search(World, mobileInfo, harvester, true, + loc => domainIndex.IsPassable(harvester.Location, loc, passable) && harvester.CanHarvestAt(loc, resLayer, harvInfo, territory)) .WithCustomCost(loc => World.FindActorsInCircle(World.Map.CenterOfCell(loc), Info.HarvesterEnemyAvoidanceRadius) - .Where(u => !u.IsDead && self.Owner.Stances[u.Owner] == Stance.Enemy) + .Where(u => !u.IsDead && harvester.Owner.Stances[u.Owner] == Stance.Enemy) .Sum(u => Math.Max(WDist.Zero.Length, Info.HarvesterEnemyAvoidanceRadius.Length - (World.Map.CenterOfCell(loc) - u.CenterPosition).Length))) - .FromPoint(self.Location)); + .FromPoint(harvester.Location)); if (path.Count == 0) return CPos.Zero; @@ -692,15 +692,15 @@ namespace OpenRA.Mods.Common.AI void GiveOrdersToIdleHarvesters() { // Find idle harvesters and give them orders: - foreach (var a in activeUnits) + foreach (var harvester in activeUnits) { - var harv = a.TraitOrDefault(); + var harv = harvester.TraitOrDefault(); if (harv == null) continue; - if (!a.IsIdle) + if (!harvester.IsIdle) { - var act = a.GetCurrentActivity(); + var act = harvester.GetCurrentActivity(); // A Wait activity is technically idle: if ((act.GetType() != typeof(Wait)) && @@ -712,9 +712,9 @@ namespace OpenRA.Mods.Common.AI continue; // Tell the idle harvester to quit slacking: - var newSafeResourcePatch = FindNextResource(a); - BotDebug("AI: Harvester {0} is idle. Ordering to {1} in search for new resources.".F(a, newSafeResourcePatch)); - QueueOrder(new Order("Harvest", a, false) { TargetLocation = newSafeResourcePatch }); + var newSafeResourcePatch = FindNextResource(harvester); + BotDebug("AI: Harvester {0} is idle. Ordering to {1} in search for new resources.".F(harvester, newSafeResourcePatch)); + QueueOrder(new Order("Harvest", harvester, false) { TargetLocation = newSafeResourcePatch }); } }