Reorganise AI base building logic.

- Now obeys defined structure percentages and limits.
- Faster.
- More readable and maintainable code.
This commit is contained in:
Paul Chote
2014-07-06 11:05:06 +12:00
parent c8bd8336f7
commit a46baeaf2b
5 changed files with 296 additions and 213 deletions

View File

@@ -33,19 +33,36 @@ namespace OpenRA.Mods.RA.AI
public readonly int RushInterval = 600;
public readonly int AttackForceInterval = 30;
[Desc("By what factor should power output exceed power consumption.")]
public readonly float ExcessPowerFactor = 1.2f;
[Desc("By what minimum amount should power output exceed power consumption.")]
public readonly int MinimumExcessPower = 50;
[Desc("How long to wait (in ticks) between structure production checks when there is no active production.")]
public readonly int StructureProductionInactiveDelay = 125;
[Desc("How long to wait (in ticks) between structure production checks ticks when actively building things.")]
public readonly int StructureProductionActiveDelay = 10;
[Desc("Minimum range at which to build defensive structures near a combat hotspot.")]
public readonly int MinimumDefenseRadius = 5;
[Desc("Maximum range at which to build defensive structures near a combat hotspot.")]
public readonly int MaximumDefenseRadius = 20;
[Desc("Try to build another production building if there is too much cash.")]
public readonly int NewProductionCashThreshold = 5000;
[Desc("Only produce units as long as there are less than this amount of units idling inside the base.")]
public readonly int IdleBaseUnitsMaximum = 12;
[Desc("Radius in cells around enemy BaseBuilder (Construction Yard) where AI scans for targets to rush.")]
public readonly int RushAttackScanRadius = 15;
[Desc("Radius in cells around the base that should be scanned for units to be protected.")]
public readonly int ProtectUnitScanRadius = 15;
[Desc("Radius in cells around a factory scanned for rally points by the AI.")]
public readonly int RallyPointScanRadius = 8;
[Desc("Radius in cells around the center of the base to expand.")]
public readonly int MaxBaseRadius = 20;
public readonly string[] UnitQueues = { "Vehicle", "Infantry", "Plane", "Ship", "Aircraft" };
public readonly bool ShouldRepairBuildings = true;
@@ -91,15 +108,16 @@ namespace OpenRA.Mods.RA.AI
public sealed class HackyAI : ITick, IBot, INotifyDamage
{
bool enabled;
public int ticks;
public Player p;
public MersenneTwister random;
public CPos baseCenter;
public MersenneTwister random { get; private set; }
public readonly HackyAIInfo Info;
public CPos baseCenter { get; private set; }
public Player p { get; private set; }
PowerManager playerPower;
SupportPowerManager supportPowerMngr;
PlayerResources playerResource;
internal readonly HackyAIInfo Info;
bool enabled;
int ticks;
HashSet<int> resourceTypeIndices;
@@ -114,7 +132,6 @@ namespace OpenRA.Mods.RA.AI
// Units that the ai already knows about. Any unit not on this list needs to be given a role.
List<Actor> activeUnits = new List<Actor>();
const int MaxBaseDistance = 40;
public const int feedbackTime = 30; // ticks; = a bit over 1s. must be >= netlag.
public readonly World world;
@@ -143,9 +160,9 @@ namespace OpenRA.Mods.RA.AI
playerResource = p.PlayerActor.Trait<PlayerResources>();
foreach (var building in Info.BuildingQueues)
builders.Add(new BaseBuilder(this, building, q => ChooseBuildingToBuild(q, false)));
builders.Add(new BaseBuilder(this, building, p, playerPower, playerResource));
foreach (var defense in Info.DefenseQueues)
builders.Add(new BaseBuilder(this, defense, q => ChooseBuildingToBuild(q, true)));
builders.Add(new BaseBuilder(this, defense, p, playerPower, playerResource));
random = new MersenneTwister((int)p.PlayerActor.ActorID);
@@ -155,12 +172,6 @@ namespace OpenRA.Mods.RA.AI
.Select(t => world.TileSet.GetTerrainIndex(t.TerrainType)));
}
static int GetPowerProvidedBy(ActorInfo building)
{
var bi = building.Traits.GetOrDefault<BuildingInfo>();
return bi != null ? bi.Power : 0;
}
ActorInfo ChooseRandomUnitToBuild(ProductionQueue queue)
{
var buildableThings = queue.BuildableItems();
@@ -182,7 +193,7 @@ namespace OpenRA.Mods.RA.AI
.Where(a => a.Actor.Owner == p)
.Select(a => a.Actor.Info.Name).ToArray();
foreach (var unit in Info.UnitsToBuild)
foreach (var unit in Info.UnitsToBuild.Shuffle(random))
if (buildableThings.Any(b => b.Name == unit.Key))
if (myUnits.Count(a => a == unit.Key) < unit.Value * myUnits.Length)
if (HasAdequateAirUnits(Map.Rules.Actors[unit.Key]))
@@ -212,7 +223,7 @@ namespace OpenRA.Mods.RA.AI
.Count(a => a.Actor.Owner == owner && Info.BuildingCommonNames[commonName].Contains(a.Actor.Info.Name));
}
ActorInfo GetBuildingInfoByCommonName(string commonName, Player owner)
public ActorInfo GetBuildingInfoByCommonName(string commonName, Player owner)
{
if (commonName == "ConstructionYard")
return Map.Rules.Actors.Where(k => Info.BuildingCommonNames[commonName].Contains(k.Key)).Random(random).Value;
@@ -220,12 +231,12 @@ namespace OpenRA.Mods.RA.AI
return GetInfoByCommonName(Info.BuildingCommonNames, commonName, owner);
}
ActorInfo GetUnitInfoByCommonName(string commonName, Player owner)
public ActorInfo GetUnitInfoByCommonName(string commonName, Player owner)
{
return GetInfoByCommonName(Info.UnitsCommonNames, commonName, owner);
}
ActorInfo GetInfoByCommonName(Dictionary<string, string[]> names, string commonName, Player owner)
public ActorInfo GetInfoByCommonName(Dictionary<string, 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));
@@ -233,28 +244,21 @@ namespace OpenRA.Mods.RA.AI
return Map.Rules.Actors.Where(k => names[commonName].Contains(k.Key)).Random(random).Value;
}
bool HasAdequatePower()
{
// note: CNC `fact` provides a small amount of power. don't get jammed because of that.
return playerPower.PowerProvided > Info.MinimumExcessPower &&
playerPower.PowerProvided > playerPower.PowerDrained * Info.ExcessPowerFactor;
}
bool HasAdequateFact()
public bool HasAdequateFact()
{
// Require at least one construction yard, unless we have no vehicles factory (can't build it).
return CountBuildingByCommonName("ConstructionYard", p) > 0 ||
CountBuildingByCommonName("VehiclesFactory", p) == 0;
}
bool HasAdequateProc()
public bool HasAdequateProc()
{
// Require at least one refinery, unless we have no power (can't build it).
return CountBuildingByCommonName("Refinery", p) > 0 ||
CountBuildingByCommonName("Power", p) == 0;
}
bool HasMinimumProc()
public bool HasMinimumProc()
{
// Require at least two refineries, unless we have no power (can't build it)
// or barracks (higher priority?)
@@ -263,14 +267,6 @@ namespace OpenRA.Mods.RA.AI
CountBuildingByCommonName("Barracks", p) == 0;
}
bool HasAdequateNumber(string frac, Player owner)
{
if (Info.BuildingLimits.ContainsKey(frac))
return CountBuilding(frac, owner) < Info.BuildingLimits[frac];
return true;
}
// For mods like RA (number of building must match the number of aircraft)
bool HasAdequateAirUnits(ActorInfo actorInfo)
{
@@ -286,108 +282,68 @@ namespace OpenRA.Mods.RA.AI
return true;
}
ActorInfo ChooseBuildingToBuild(ProductionQueue queue, bool isDefense)
{
var buildableThings = queue.BuildableItems();
if (!isDefense)
{
// Try to maintain 20% excess power
if (!HasAdequatePower())
return buildableThings.Where(a => GetPowerProvidedBy(a) > 0)
.MaxByOrDefault(a => GetPowerProvidedBy(a));
if (playerResource.AlertSilo)
return GetBuildingInfoByCommonName("Silo", p);
if (!HasAdequateProc() || !HasMinimumProc())
return GetBuildingInfoByCommonName("Refinery", p);
}
var myBuildings = p.World
.ActorsWithTrait<Building>()
.Where(a => a.Actor.Owner == p)
.Select(a => a.Actor.Info.Name)
.ToArray();
foreach (var frac in Info.BuildingFractions)
if (buildableThings.Any(b => b.Name == frac.Key))
if (myBuildings.Count(a => a == frac.Key) < frac.Value * myBuildings.Length && HasAdequateNumber(frac.Key, p) &&
playerPower.ExcessPower >= Map.Rules.Actors[frac.Key].Traits.Get<BuildingInfo>().Power)
return Map.Rules.Actors[frac.Key];
return null;
}
bool NoBuildingsUnder(IEnumerable<CPos> cells)
{
var bi = world.WorldActor.Trait<BuildingInfluence>();
return cells.All(c => bi.GetBuildingAt(c) == null);
}
CPos defenseCenter;
public CPos? ChooseBuildLocation(string actorType, BuildingType type)
{
return ChooseBuildLocation(actorType, true, MaxBaseDistance, type);
}
public CPos? ChooseBuildLocation(string actorType, bool distanceToBaseIsImportant, int maxBaseDistance, BuildingType type)
public CPos? ChooseBuildLocation(string actorType, bool distanceToBaseIsImportant, BuildingType type)
{
var bi = Map.Rules.Actors[actorType].Traits.GetOrDefault<BuildingInfo>();
if (bi == null)
return null;
Func<WPos, CPos, CPos?> findPos = (pos, center) =>
// Find the buildable cell that is closest to pos and centered around center
Func<CPos, CPos, int, int, CPos?> findPos = (center, target, minRange, maxRange) =>
{
for (var k = MaxBaseDistance; k >= 0; k--)
{
var tlist = Map.FindTilesInCircle(center, k);
var cells = Map.FindTilesInAnnulus(center, minRange, maxRange);
foreach (var t in tlist)
if (world.CanPlaceBuilding(actorType, bi, t, null))
if (bi.IsCloseEnoughToBase(world, p, actorType, t))
if (NoBuildingsUnder(Util.ExpandFootprint(FootprintUtils.Tiles(Map.Rules, actorType, bi, t), false)))
return t;
// Sort by distance to target if we have one
if (center != target)
cells = cells.OrderBy(c => (center - target).LengthSquared);
else
cells = cells.Shuffle(random);
foreach (var cell in cells)
{
if (!world.CanPlaceBuilding(actorType, bi, cell, null))
continue;
if (distanceToBaseIsImportant && !bi.IsCloseEnoughToBase(world, p, actorType, cell))
continue;
return cell;
}
return null;
};
var baseCenterPos = world.Map.CenterOfCell(baseCenter);
switch (type)
{
case BuildingType.Defense:
var enemyBase = FindEnemyBuildingClosestToPos(baseCenterPos);
return enemyBase != null ? findPos(enemyBase.CenterPosition, defenseCenter) : null;
// Build near the closest enemy structure
var closestEnemy = world.Actors.Where(a => !a.Destroyed && a.HasTrait<Building>() && p.Stances[a.Owner] == Stance.Enemy)
.ClosestTo(world.Map.CenterOfCell(defenseCenter));
var targetCell = closestEnemy != null ? closestEnemy.Location : baseCenter;
return findPos(defenseCenter, targetCell, Info.MinimumDefenseRadius, Info.MaximumDefenseRadius);
case BuildingType.Refinery:
var tilesPos = Map.FindTilesInCircle(baseCenter, MaxBaseDistance)
.Where(a => resourceTypeIndices.Contains(Map.GetTerrainIndex(new CPos(a.X, a.Y))));
if (tilesPos.Any())
{
var pos = tilesPos.MinBy(a => (world.Map.CenterOfCell(a) - baseCenterPos).LengthSquared);
return findPos(world.Map.CenterOfCell(pos), baseCenter);
}
return null;
// Try and place the refinery near a resource field
var nearbyResources = Map.FindTilesInCircle(baseCenter, Info.MaxBaseRadius)
.Where(a => resourceTypeIndices.Contains(Map.GetTerrainIndex(a)))
.Shuffle(random);
foreach (var c in nearbyResources)
{
var found = findPos(c, baseCenter, 0, Info.MaxBaseRadius);
if (found != null)
return found;
}
// Try and find a free spot somewhere else in the base
return findPos(baseCenter, baseCenter, 0, Info.MaxBaseRadius);
case BuildingType.Building:
for (var k = 0; k < maxBaseDistance; k++)
{
foreach (var t in Map.FindTilesInCircle(baseCenter, k))
{
if (world.CanPlaceBuilding(actorType, bi, t, null))
{
if (distanceToBaseIsImportant && !bi.IsCloseEnoughToBase(world, p, actorType, t))
continue;
if (NoBuildingsUnder(Util.ExpandFootprint(FootprintUtils.Tiles(Map.Rules, actorType, bi, t), false)))
return t;
}
}
}
break;
return findPos(baseCenter, baseCenter, 0, distanceToBaseIsImportant ? Info.MaxBaseRadius : Map.MaxTilesInCircleRange);
}
// Can't find a build location
@@ -481,14 +437,6 @@ namespace OpenRA.Mods.RA.AI
&& a.HasTrait<BaseBuilding>() && !a.HasTrait<Mobile>()).ToList();
}
Actor FindEnemyBuildingClosestToPos(WPos pos)
{
var closestBuilding = world.Actors.Where(a => p.Stances[a.Owner] == Stance.Enemy
&& !a.Destroyed && a.HasTrait<Building>()).ClosestTo(pos);
return closestBuilding;
}
void CleanSquads()
{
squads.RemoveAll(s => !s.IsValid);
@@ -718,8 +666,6 @@ namespace OpenRA.Mods.RA.AI
// backup location within the main base.
void FindAndDeployBackupMcv(Actor self)
{
var maxBaseDistance = Math.Max(world.Map.MapSize.X, world.Map.MapSize.Y);
// HACK: This needs to query against MCVs directly
var mcvs = self.World.Actors.Where(a => a.Owner == p && a.HasTrait<BaseBuilding>() && a.HasTrait<Mobile>());
if (!mcvs.Any())
@@ -731,7 +677,7 @@ namespace OpenRA.Mods.RA.AI
continue;
var factType = mcv.Info.Traits.Get<TransformsInfo>().IntoActor;
var desiredLocation = ChooseBuildLocation(factType, false, maxBaseDistance, BuildingType.Building);
var desiredLocation = ChooseBuildLocation(factType, false, BuildingType.Building);
if (desiredLocation == null)
continue;