Merge pull request #10506 from pchote/fix-flying-mcvs

Remove some hardcoded trait assumptions from HackyAI.
This commit is contained in:
Matthias Mailänder
2016-01-17 08:18:04 +01:00
2 changed files with 81 additions and 78 deletions

View File

@@ -170,12 +170,8 @@ namespace OpenRA.Mods.Common.AI
return true; return true;
} }
ActorInfo GetProducibleBuilding(string commonName, IEnumerable<ActorInfo> buildables, Func<ActorInfo, int> orderBy = null) ActorInfo GetProducibleBuilding(HashSet<string> actors, IEnumerable<ActorInfo> buildables, Func<ActorInfo, int> orderBy = null)
{ {
HashSet<string> actors;
if (!ai.Info.BuildingCommonNames.TryGetValue(commonName, out actors))
return null;
var available = buildables.Where(actor => var available = buildables.Where(actor =>
{ {
// Are we able to build this? // Are we able to build this?
@@ -205,7 +201,7 @@ namespace OpenRA.Mods.Common.AI
var buildableThings = queue.BuildableItems(); var buildableThings = queue.BuildableItems();
// This gets used quite a bit, so let's cache it here // 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<PowerInfo>().Where(i => i.UpgradeMinEnabledLevel < 1).Sum(p => p.Amount)); a => a.TraitInfos<PowerInfo>().Where(i => i.UpgradeMinEnabledLevel < 1).Sum(p => p.Amount));
// First priority is to get out of a low power situation // 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 // Next is to build up a strong economy
if (!ai.HasAdequateProc() || !ai.HasMinimumProc()) if (!ai.HasAdequateProc() || !ai.HasMinimumProc())
{ {
var refinery = GetProducibleBuilding("Refinery", buildableThings); var refinery = GetProducibleBuilding(ai.Info.BuildingCommonNames.Refinery, buildableThings);
if (refinery != null && HasSufficientPowerForActor(refinery)) if (refinery != null && HasSufficientPowerForActor(refinery))
{ {
HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (refinery)", queue.Actor.Owner, refinery.Name); 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 // Make sure that we can spend as fast as we are earning
if (ai.Info.NewProductionCashThreshold > 0 && playerResources.Resources > ai.Info.NewProductionCashThreshold) 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)) if (production != null && HasSufficientPowerForActor(production))
{ {
HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (production)", queue.Actor.Owner, production.Name); 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 && playerResources.Resources > ai.Info.NewProductionCashThreshold
&& ai.CloseEnoughToWater()) && ai.CloseEnoughToWater())
{ {
var navalproduction = GetProducibleBuilding("NavalProduction", buildableThings); var navalproduction = GetProducibleBuilding(ai.Info.BuildingCommonNames.NavalProduction, buildableThings);
if (navalproduction != null && HasSufficientPowerForActor(navalproduction)) if (navalproduction != null && HasSufficientPowerForActor(navalproduction))
{ {
HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (navalproduction)", queue.Actor.Owner, navalproduction.Name); 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 // Create some head room for resource storage if we really need it
if (playerResources.Resources > 0.8 * playerResources.ResourceCapacity) 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)) if (silo != null && HasSufficientPowerForActor(silo))
{ {
HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (silo)", queue.Actor.Owner, silo.Name); 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 // 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. // and any structure providing buildable area close enough to that water.
// TODO: Extend this check to cover any naval structure, not just production. // TODO: Extend this check to cover any naval structure, not just production.
if (ai.Info.BuildingCommonNames.ContainsKey("NavalProduction") if (ai.Info.BuildingCommonNames.NavalProduction.Contains(name)
&& ai.Info.BuildingCommonNames["NavalProduction"].Contains(name)
&& (waterState == Water.NotEnoughWater || !ai.CloseEnoughToWater())) && (waterState == Water.NotEnoughWater || !ai.CloseEnoughToWater()))
continue; continue;

View File

@@ -23,6 +23,23 @@ namespace OpenRA.Mods.Common.AI
{ {
public sealed class HackyAIInfo : IBotInfo, ITraitInfo public sealed class HackyAIInfo : IBotInfo, ITraitInfo
{ {
public class UnitCategories
{
public readonly HashSet<string> Mcv = new HashSet<string>();
}
public class BuildingCategories
{
public readonly HashSet<string> ConstructionYard = new HashSet<string>();
public readonly HashSet<string> VehiclesFactory = new HashSet<string>();
public readonly HashSet<string> Refinery = new HashSet<string>();
public readonly HashSet<string> Power = new HashSet<string>();
public readonly HashSet<string> Barracks = new HashSet<string>();
public readonly HashSet<string> Production = new HashSet<string>();
public readonly HashSet<string> NavalProduction = new HashSet<string>();
public readonly HashSet<string> Silo = new HashSet<string>();
}
[Desc("Ingame name this bot uses.")] [Desc("Ingame name this bot uses.")]
public readonly string Name = "Unnamed Bot"; public readonly string Name = "Unnamed Bot";
@@ -133,11 +150,13 @@ namespace OpenRA.Mods.Common.AI
public readonly Dictionary<string, float> BuildingFractions = null; public readonly Dictionary<string, float> BuildingFractions = null;
[Desc("Tells the AI what unit types fall under the same common name. Only supported entry is Mcv.")] [Desc("Tells the AI what unit types fall under the same common name. Only supported entry is Mcv.")]
public readonly Dictionary<string, HashSet<string>> UnitsCommonNames = null; [FieldLoader.LoadUsing("LoadUnitCategories", true)]
public readonly UnitCategories UnitsCommonNames;
[Desc("Tells the AI what building types fall under the same common name.", [Desc("Tells the AI what building types fall under the same common name.",
"Possible keys are ConstructionYard, Power, Refinery, Silo , Barracks, Production, VehiclesFactory, NavalProduction.")] "Possible keys are ConstructionYard, Power, Refinery, Silo , Barracks, Production, VehiclesFactory, NavalProduction.")]
public readonly Dictionary<string, HashSet<string>> BuildingCommonNames = null; [FieldLoader.LoadUsing("LoadBuildingCategories", true)]
public readonly BuildingCategories BuildingCommonNames;
[Desc("What buildings should the AI have a maximum limit to build.")] [Desc("What buildings should the AI have a maximum limit to build.")]
public readonly Dictionary<string, int> BuildingLimits = null; public readonly Dictionary<string, int> BuildingLimits = null;
@@ -147,6 +166,18 @@ namespace OpenRA.Mods.Common.AI
[FieldLoader.LoadUsing("LoadDecisions")] [FieldLoader.LoadUsing("LoadDecisions")]
public readonly List<SupportPowerDecision> PowerDecisions = new List<SupportPowerDecision>(); public readonly List<SupportPowerDecision> PowerDecisions = new List<SupportPowerDecision>();
static object LoadUnitCategories(MiniYaml yaml)
{
var categories = yaml.Nodes.First(n => n.Key == "UnitsCommonNames");
return FieldLoader.Load<UnitCategories>(categories.Value);
}
static object LoadBuildingCategories(MiniYaml yaml)
{
var categories = yaml.Nodes.First(n => n.Key == "BuildingCommonNames");
return FieldLoader.Load<BuildingCategories>(categories.Value);
}
static object LoadDecisions(MiniYaml yaml) static object LoadDecisions(MiniYaml yaml)
{ {
var ret = new List<SupportPowerDecision>(); var ret = new List<SupportPowerDecision>();
@@ -171,11 +202,11 @@ namespace OpenRA.Mods.Common.AI
public CPos GetRandomBaseCenter() public CPos GetRandomBaseCenter()
{ {
var randomBaseBuilding = World.ActorsHavingTrait<BaseBuilding>() var randomConstructionYard = World.Actors.Where(a => a.Owner == Player &&
.Where(a => a.Owner == Player && !a.Info.HasTraitInfo<MobileInfo>()) Info.BuildingCommonNames.ConstructionYard.Contains(a.Info.Name))
.RandomOrDefault(Random); .RandomOrDefault(Random);
return randomBaseBuilding != null ? randomBaseBuilding.Location : initialBaseCenter; return randomConstructionYard != null ? randomConstructionYard.Location : initialBaseCenter;
} }
public bool IsEnabled; public bool IsEnabled;
@@ -287,7 +318,7 @@ namespace OpenRA.Mods.Common.AI
public bool EnoughWaterToBuildNaval() public bool EnoughWaterToBuildNaval()
{ {
var baseProviders = World.ActorsHavingTrait<BaseProvider>() var baseProviders = World.ActorsHavingTrait<BaseProvider>()
.Where(a => a.Owner == Player && !a.Info.HasTraitInfo<MobileInfo>()); .Where(a => a.Owner == Player);
foreach (var b in baseProviders) foreach (var b in baseProviders)
{ {
@@ -312,7 +343,7 @@ namespace OpenRA.Mods.Common.AI
public bool CloseEnoughToWater() public bool CloseEnoughToWater()
{ {
var areaProviders = World.ActorsHavingTrait<GivesBuildableArea>() var areaProviders = World.ActorsHavingTrait<GivesBuildableArea>()
.Where(a => a.Owner == Player && !a.Info.HasTraitInfo<MobileInfo>()); .Where(a => a.Owner == Player);
foreach (var a in areaProviders) foreach (var a in areaProviders)
{ {
@@ -378,57 +409,38 @@ namespace OpenRA.Mods.Common.AI
return World.ActorsHavingTrait<IPositionable>().Count(a => a.Owner == owner && a.Info.Name == unit); return World.ActorsHavingTrait<IPositionable>().Count(a => a.Owner == owner && a.Info.Name == unit);
} }
int? CountBuildingByCommonName(string commonName, Player owner) int CountBuildingByCommonName(HashSet<string> buildings, Player owner)
{ {
if (!Info.BuildingCommonNames.ContainsKey(commonName))
return null;
return World.ActorsHavingTrait<Building>() return World.ActorsHavingTrait<Building>()
.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<string> names, Player owner)
{ {
if (commonName == "ConstructionYard") return Map.Rules.Actors.Where(k => names.Contains(k.Key)).Random(Random).Value;
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<string, HashSet<string>> 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;
} }
public bool HasAdequateFact() public bool HasAdequateFact()
{ {
// Require at least one construction yard, unless we have no vehicles factory (can't build it). // Require at least one construction yard, unless we have no vehicles factory (can't build it).
return CountBuildingByCommonName("ConstructionYard", Player) > 0 || return CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) > 0 ||
CountBuildingByCommonName("VehiclesFactory", Player) == 0; CountBuildingByCommonName(Info.BuildingCommonNames.VehiclesFactory, Player) == 0;
} }
public bool HasAdequateProc() public bool HasAdequateProc()
{ {
// Require at least one refinery, unless we have no power (can't build it). // Require at least one refinery, unless we have no power (can't build it).
return CountBuildingByCommonName("Refinery", Player) > 0 || return CountBuildingByCommonName(Info.BuildingCommonNames.Refinery, Player) > 0 ||
CountBuildingByCommonName("Power", Player) == 0; CountBuildingByCommonName(Info.BuildingCommonNames.Power, Player) == 0;
} }
public bool HasMinimumProc() public bool HasMinimumProc()
{ {
// Require at least two refineries, unless we have no power (can't build it) // Require at least two refineries, unless we have no power (can't build it)
// or barracks (higher priority?) // or barracks (higher priority?)
return CountBuildingByCommonName("Refinery", Player) >= 2 || return CountBuildingByCommonName(Info.BuildingCommonNames.Refinery, Player) >= 2 ||
CountBuildingByCommonName("Power", Player) == 0 || CountBuildingByCommonName(Info.BuildingCommonNames.Power, Player) == 0 ||
CountBuildingByCommonName("Barracks", Player) == 0; CountBuildingByCommonName(Info.BuildingCommonNames.Barracks, Player) == 0;
} }
// For mods like RA (number of building must match the number of aircraft) // For mods like RA (number of building must match the number of aircraft)
@@ -597,8 +609,8 @@ namespace OpenRA.Mods.Common.AI
List<Actor> FindEnemyConstructionYards() List<Actor> FindEnemyConstructionYards()
{ {
return World.ActorsHavingTrait<BaseBuilding>() return World.Actors.Where(a => Player.Stances[a.Owner] == Stance.Enemy && !a.IsDead &&
.Where(a => Player.Stances[a.Owner] == Stance.Enemy && !a.IsDead && !a.Info.HasTraitInfo<MobileInfo>()).ToList(); Info.BuildingCommonNames.ConstructionYard.Contains(a.Info.Name)).ToList();
} }
void CleanSquads() void CleanSquads()
@@ -657,19 +669,19 @@ namespace OpenRA.Mods.Common.AI
FindAndDeployBackupMcv(self); FindAndDeployBackupMcv(self);
} }
CPos FindNextResource(Actor self) CPos FindNextResource(Actor harvester)
{ {
var harvInfo = self.Info.TraitInfo<HarvesterInfo>(); var harvInfo = harvester.Info.TraitInfo<HarvesterInfo>();
var mobileInfo = self.Info.TraitInfo<MobileInfo>(); var mobileInfo = harvester.Info.TraitInfo<MobileInfo>();
var passable = (uint)mobileInfo.GetMovementClass(World.TileSet); var passable = (uint)mobileInfo.GetMovementClass(World.TileSet);
var path = pathfinder.FindPath( var path = pathfinder.FindPath(
PathSearch.Search(World, mobileInfo, self, true, PathSearch.Search(World, mobileInfo, harvester, true,
loc => domainIndex.IsPassable(self.Location, loc, passable) && self.CanHarvestAt(loc, resLayer, harvInfo, territory)) loc => domainIndex.IsPassable(harvester.Location, loc, passable) && harvester.CanHarvestAt(loc, resLayer, harvInfo, territory))
.WithCustomCost(loc => World.FindActorsInCircle(World.Map.CenterOfCell(loc), Info.HarvesterEnemyAvoidanceRadius) .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))) .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) if (path.Count == 0)
return CPos.Zero; return CPos.Zero;
@@ -680,15 +692,15 @@ namespace OpenRA.Mods.Common.AI
void GiveOrdersToIdleHarvesters() void GiveOrdersToIdleHarvesters()
{ {
// Find idle harvesters and give them orders: // Find idle harvesters and give them orders:
foreach (var a in activeUnits) foreach (var harvester in activeUnits)
{ {
var harv = a.TraitOrDefault<Harvester>(); var harv = harvester.TraitOrDefault<Harvester>();
if (harv == null) if (harv == null)
continue; continue;
if (!a.IsIdle) if (!harvester.IsIdle)
{ {
var act = a.GetCurrentActivity(); var act = harvester.GetCurrentActivity();
// A Wait activity is technically idle: // A Wait activity is technically idle:
if ((act.GetType() != typeof(Wait)) && if ((act.GetType() != typeof(Wait)) &&
@@ -700,16 +712,16 @@ namespace OpenRA.Mods.Common.AI
continue; continue;
// Tell the idle harvester to quit slacking: // Tell the idle harvester to quit slacking:
var newSafeResourcePatch = FindNextResource(a); var newSafeResourcePatch = FindNextResource(harvester);
BotDebug("AI: Harvester {0} is idle. Ordering to {1} in search for new resources.".F(a, newSafeResourcePatch)); BotDebug("AI: Harvester {0} is idle. Ordering to {1} in search for new resources.".F(harvester, newSafeResourcePatch));
QueueOrder(new Order("Harvest", a, false) { TargetLocation = newSafeResourcePatch }); QueueOrder(new Order("Harvest", harvester, false) { TargetLocation = newSafeResourcePatch });
} }
} }
void FindNewUnits(Actor self) void FindNewUnits(Actor self)
{ {
var newUnits = self.World.ActorsHavingTrait<IPositionable>() var newUnits = self.World.ActorsHavingTrait<IPositionable>()
.Where(a => a.Owner == Player && !a.Info.HasTraitInfo<BaseBuildingInfo>() && !activeUnits.Contains(a)); .Where(a => a.Owner == Player && !Info.UnitsCommonNames.Mcv.Contains(a.Info.Name) && !activeUnits.Contains(a));
foreach (var a in newUnits) foreach (var a in newUnits)
{ {
@@ -833,16 +845,13 @@ namespace OpenRA.Mods.Common.AI
void InitializeBase(Actor self) void InitializeBase(Actor self)
{ {
// Find and deploy our mcv // Find and deploy our mcv
var mcv = self.World.ActorsHavingTrait<BaseBuilding>().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) if (mcv != null)
{ {
initialBaseCenter = mcv.Location; initialBaseCenter = mcv.Location;
defenseCenter = 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<MobileInfo>())
QueueOrder(new Order("DeployTransform", mcv, false)); QueueOrder(new Order("DeployTransform", mcv, false));
} }
else else
@@ -853,9 +862,8 @@ namespace OpenRA.Mods.Common.AI
// backup location within the main base. // backup location within the main base.
void FindAndDeployBackupMcv(Actor self) void FindAndDeployBackupMcv(Actor self)
{ {
// HACK: This needs to query against MCVs directly var mcvs = self.World.Actors.Where(a => a.Owner == Player &&
var mcvs = self.World.ActorsHavingTrait<BaseBuilding>() Info.UnitsCommonNames.Mcv.Contains(a.Info.Name));
.Where(a => a.Owner == Player && a.Info.HasTraitInfo<MobileInfo>());
foreach (var mcv in mcvs) foreach (var mcv in mcvs)
{ {
@@ -1012,9 +1020,9 @@ namespace OpenRA.Mods.Common.AI
return; return;
// No construction yards - Build a new MCV // No construction yards - Build a new MCV
if (!HasAdequateFact() && !self.World.ActorsHavingTrait<BaseBuilding>() if (!HasAdequateFact() && !self.World.Actors.Any(a => a.Owner == Player &&
.Any(a => a.Owner == Player && a.Info.HasTraitInfo<MobileInfo>())) Info.UnitsCommonNames.Mcv.Contains(a.Info.Name)))
BuildUnit("Vehicle", GetUnitInfoByCommonName("Mcv", Player).Name); BuildUnit("Vehicle", GetInfoByCommonName(Info.UnitsCommonNames.Mcv, Player).Name);
foreach (var q in Info.UnitQueues) foreach (var q in Info.UnitQueues)
BuildUnit(q, unitsHangingAroundTheBase.Count < Info.IdleBaseUnitsMaximum); BuildUnit(q, unitsHangingAroundTheBase.Count < Info.IdleBaseUnitsMaximum);