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

@@ -9,95 +9,215 @@
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Mods.RA.Buildings;
using OpenRA.Traits;
namespace OpenRA.Mods.RA.AI
{
class BaseBuilder
{
enum BuildState { ChooseItem, WaitForProduction, WaitForFeedback }
readonly string category;
BuildState state = BuildState.WaitForFeedback;
string category;
HackyAI ai;
int lastThinkTick;
Func<ProductionQueue, ActorInfo> chooseItem;
readonly HackyAI ai;
readonly World world;
readonly Player player;
readonly PowerManager playerPower;
readonly PlayerResources playerResources;
public BaseBuilder(HackyAI ai, string category, Func<ProductionQueue, ActorInfo> chooseItem)
int waitTicks;
Actor[] playerBuildings;
public BaseBuilder(HackyAI ai, string category, Player p, PowerManager pm, PlayerResources pr)
{
this.ai = ai;
world = p.World;
player = p;
playerPower = pm;
playerResources = pr;
this.category = category;
this.chooseItem = chooseItem;
}
public void Tick()
{
// Pick a free queue
var queue = ai.FindQueues(category).FirstOrDefault();
if (queue == null)
// Only update once per second or so
if (--waitTicks > 0)
return;
playerBuildings = world.ActorsWithTrait<Building>()
.Where(a => a.Actor.Owner == player)
.Select(a => a.Actor)
.ToArray();
var active = false;
foreach (var queue in ai.FindQueues(category))
if (TickQueue(queue))
active = true;
waitTicks = active ? ai.Info.StructureProductionActiveDelay : ai.Info.StructureProductionInactiveDelay;
}
bool TickQueue(ProductionQueue queue)
{
var currentBuilding = queue.CurrentItem();
switch (state)
// Waiting to build something
if (currentBuilding == null)
{
case BuildState.ChooseItem:
var item = chooseItem(queue);
var item = ChooseBuildingToBuild(queue);
if (item == null)
return false;
HackyAI.BotDebug("AI: {0} is starting production of {1}".F(player, item.Name));
world.IssueOrder(Order.StartProduction(queue.Actor, item.Name, 1));
}
// Production is complete
else if (currentBuilding.Done)
{
// Choose the placement logic
// HACK: HACK HACK HACK
var type = BuildingType.Building;
if (world.Map.Rules.Actors[currentBuilding.Item].Traits.Contains<AttackBaseInfo>())
type = BuildingType.Defense;
else if (world.Map.Rules.Actors[currentBuilding.Item].Traits.Contains<OreRefineryInfo>())
type = BuildingType.Refinery;
var location = ai.ChooseBuildLocation(currentBuilding.Item, true, type);
if (location == null)
{
state = BuildState.WaitForFeedback;
lastThinkTick = ai.ticks;
HackyAI.BotDebug("AI: {0} has nowhere to place {1}".F(player, currentBuilding.Item));
world.IssueOrder(Order.CancelProduction(queue.Actor, currentBuilding.Item, 1));
}
else
{
HackyAI.BotDebug("AI: Starting production of {0}".F(item.Name));
state = BuildState.WaitForProduction;
ai.world.IssueOrder(Order.StartProduction(queue.Actor, item.Name, 1));
}
break;
case BuildState.WaitForProduction:
if (currentBuilding == null)
return;
if (currentBuilding.Paused)
ai.world.IssueOrder(Order.PauseProduction(queue.Actor, currentBuilding.Item, false));
else if (currentBuilding.Done)
{
state = BuildState.WaitForFeedback;
lastThinkTick = ai.ticks;
// Place the building
var type = BuildingType.Building;
if (ai.Map.Rules.Actors[currentBuilding.Item].Traits.Contains<AttackBaseInfo>())
type = BuildingType.Defense;
else if (ai.Map.Rules.Actors[currentBuilding.Item].Traits.Contains<OreRefineryInfo>())
type = BuildingType.Refinery;
var location = ai.ChooseBuildLocation(currentBuilding.Item, type);
if (location == null)
world.IssueOrder(new Order("PlaceBuilding", player.PlayerActor, false)
{
HackyAI.BotDebug("AI: Nowhere to place {0}".F(currentBuilding.Item));
ai.world.IssueOrder(Order.CancelProduction(queue.Actor, currentBuilding.Item, 1));
}
else
{
ai.world.IssueOrder(new Order("PlaceBuilding", ai.p.PlayerActor, false)
{
TargetLocation = location.Value,
TargetString = currentBuilding.Item,
TargetActor = queue.Actor,
SuppressVisualFeedback = true
});
}
TargetLocation = location.Value,
TargetString = currentBuilding.Item,
TargetActor = queue.Actor,
SuppressVisualFeedback = true
});
return true;
}
break;
case BuildState.WaitForFeedback:
if (ai.ticks - lastThinkTick > HackyAI.feedbackTime)
state = BuildState.ChooseItem;
break;
}
return true;
}
ActorInfo GetProducibleBuilding(string commonName, IEnumerable<ActorInfo> buildables, Func<ActorInfo, int> orderBy = null)
{
string[] actors;
if (!ai.Info.BuildingCommonNames.TryGetValue(commonName, out actors))
throw new InvalidOperationException("Can't find {0} in the HackyAI BuildingCommonNames definition.".F(commonName));
var available = buildables.Where(actor =>
{
// Are we able to build this?
if (!actors.Contains(actor.Name))
return false;
if (!ai.Info.BuildingLimits.ContainsKey(actor.Name))
return true;
return playerBuildings.Count(a => a.Info.Name == actor.Name) <= ai.Info.BuildingLimits[actor.Name];
});
if (orderBy != null)
return available.MaxByOrDefault(orderBy);
return available.RandomOrDefault(ai.random);
}
ActorInfo ChooseBuildingToBuild(ProductionQueue queue)
{
var buildableThings = queue.BuildableItems();
// First priority is to get out of a low power situation
if (playerPower.ExcessPower < 0)
{
var power = GetProducibleBuilding("Power", buildableThings, a => a.Traits.Get<BuildingInfo>().Power);
if (power != null && power.Traits.Get<BuildingInfo>().Power > 0)
{
HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (low power)", queue.Actor.Owner, power.Name);
return power;
}
}
// Next is to build up a strong economy
if (!ai.HasAdequateProc() || !ai.HasMinimumProc())
{
var refinery = GetProducibleBuilding("Refinery", buildableThings);
if (refinery != null)
{
HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (refinery)", queue.Actor.Owner, refinery.Name);
return refinery;
}
}
// Make sure that we can can spend as fast as we are earning
if (ai.Info.NewProductionCashThreshold > 0 && playerResources.Resources > ai.Info.NewProductionCashThreshold)
{
var production = GetProducibleBuilding("Production", buildableThings);
if (production != null)
{
HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (production)", queue.Actor.Owner, production.Name);
return production;
}
}
// Create some head room for resource storage if we really need it
if (playerResources.AlertSilo)
{
var silo = GetProducibleBuilding("Silo", buildableThings);
if (silo != null)
{
HackyAI.BotDebug("AI: {0} decided to build {1}: Priority override (silo)", queue.Actor.Owner, silo.Name);
return silo;
}
}
// Build everything else
foreach (var frac in ai.Info.BuildingFractions.Shuffle(ai.random))
{
var name = frac.Key;
// Can we build this structure?
if (!buildableThings.Any(b => b.Name == name))
continue;
// Do we want to build this structure?
var count = playerBuildings.Count(a => a.Info.Name == name);
if (count > frac.Value * playerBuildings.Length)
continue;
if (ai.Info.BuildingLimits.ContainsKey(name) && ai.Info.BuildingLimits[name] <= count)
continue;
// Will this put us into low power?
var actor = world.Map.Rules.Actors[frac.Key];
var bi = actor.Traits.Get<BuildingInfo>();
if (playerPower.ExcessPower < 0 || playerPower.ExcessPower < bi.Power)
{
// Try building a power plant instead
var power = GetProducibleBuilding("Power", buildableThings, a => a.Traits.Get<BuildingInfo>().Power);
if (power != null && power.Traits.Get<BuildingInfo>().Power > 0)
{
HackyAI.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name);
return power;
}
}
// Lets build this
HackyAI.BotDebug("{0} decided to build {1}: Desired is {2} ({3} / {4}); current is {5} / {4}", queue.Actor.Owner, name, frac.Value, frac.Value * playerBuildings.Length, playerBuildings.Length, count);
return actor;
}
// Too spammy to keep enabled all the time, but very useful when debugging specific issues.
// HackyAI.BotDebug("{0} couldn't decide what to build for queue {1}.", queue.Actor.Owner, queue.Info.Group);
return null;
}
}
}