public void Tick() { // If failed to place something N consecutive times, wait M ticks until resuming building production if (failCount >= ai.Info.MaximumFailedPlacementAttempts && --failRetryTicks <= 0) { var currentBuildings = world.ActorsHavingTrait <Building>().Count(a => a.Owner == player); var baseProviders = world.ActorsHavingTrait <BaseProvider>().Count(a => a.Owner == player); // Only bother resetting failCount if either a) the number of buildings has decreased since last failure M ticks ago, // or b) number of BaseProviders (construction yard or similar) has increased since then. // Otherwise reset failRetryTicks instead to wait again. if (currentBuildings < cachedBuildings || baseProviders > cachedBases) { failCount = 0; } else { failRetryTicks = ai.Info.StructureProductionResumeDelay; } } if (waterState == Water.NotChecked) { if (AIUtils.IsAreaAvailable <BaseProvider>(ai.World, ai.Player, ai.Map, ai.Info.MaxBaseRadius, ai.Info.WaterTerrainTypes)) { waterState = Water.EnoughWater; } else { waterState = Water.NotEnoughWater; checkForBasesTicks = ai.Info.CheckForNewBasesDelay; } } if (waterState == Water.NotEnoughWater && --checkForBasesTicks <= 0) { var currentBases = world.ActorsHavingTrait <BaseProvider>().Count(a => a.Owner == player); if (currentBases > cachedBases) { cachedBases = currentBases; waterState = Water.NotChecked; } } // Only update once per second or so if (--waitTicks > 0) { return; } playerBuildings = world.ActorsHavingTrait <Building>().Where(a => a.Owner == player).ToArray(); var excessPowerBonus = ai.Info.ExcessPowerIncrement * (playerBuildings.Count() / ai.Info.ExcessPowerIncreaseThreshold.Clamp(1, int.MaxValue)); minimumExcessPower = (ai.Info.MinimumExcessPower + excessPowerBonus).Clamp(ai.Info.MinimumExcessPower, ai.Info.MaximumExcessPower); var active = false; foreach (var queue in ai.FindQueues(category)) { if (TickQueue(queue)) { active = true; } } // Add a random factor so not every AI produces at the same tick early in the game. // Minimum should not be negative as delays in HackyAI could be zero. var randomFactor = ai.Random.Next(0, ai.Info.StructureProductionRandomBonusDelay); // Needs to be at least 4 * OrderLatency because otherwise the AI frequently duplicates build orders (i.e. makes the same build decision twice) waitTicks = active ? 4 * world.LobbyInfo.GlobalSettings.OrderLatency + ai.Info.StructureProductionActiveDelay + randomFactor : ai.Info.StructureProductionInactiveDelay + randomFactor; }
ActorInfo ChooseBuildingToBuild(ProductionQueue queue) { var buildableThings = queue.BuildableItems(); // This gets used quite a bit, so let's cache it here var power = GetProducibleBuilding(ai.Info.BuildingCommonNames.Power, buildableThings, a => a.TraitInfos <PowerInfo>().Where(i => i.EnabledByDefault).Sum(p => p.Amount)); // First priority is to get out of a low power situation if (playerPower != null && playerPower.ExcessPower < minimumExcessPower) { if (power != null && power.TraitInfos <PowerInfo>().Where(i => i.EnabledByDefault).Sum(p => p.Amount) > 0) { AIUtils.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(ai.Info.BuildingCommonNames.Refinery, buildableThings); if (refinery != null && HasSufficientPowerForActor(refinery)) { AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (refinery)", queue.Actor.Owner, refinery.Name); return(refinery); } if (power != null && refinery != null && !HasSufficientPowerForActor(refinery)) { AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); return(power); } } // 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(ai.Info.BuildingCommonNames.Production, buildableThings); if (production != null && HasSufficientPowerForActor(production)) { AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (production)", queue.Actor.Owner, production.Name); return(production); } if (power != null && production != null && !HasSufficientPowerForActor(production)) { AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); return(power); } } // Only consider building this if there is enough water inside the base perimeter and there are close enough adjacent buildings if (waterState == Water.EnoughWater && ai.Info.NewProductionCashThreshold > 0 && playerResources.Resources > ai.Info.NewProductionCashThreshold && AIUtils.IsAreaAvailable <GivesBuildableArea>(ai.World, ai.Player, ai.Map, ai.Info.CheckForWaterRadius, ai.Info.WaterTerrainTypes)) { var navalproduction = GetProducibleBuilding(ai.Info.BuildingCommonNames.NavalProduction, buildableThings); if (navalproduction != null && HasSufficientPowerForActor(navalproduction)) { AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (navalproduction)", queue.Actor.Owner, navalproduction.Name); return(navalproduction); } if (power != null && navalproduction != null && !HasSufficientPowerForActor(navalproduction)) { AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); return(power); } } // Create some head room for resource storage if we really need it if (playerResources.Resources > 0.8 * playerResources.ResourceCapacity) { var silo = GetProducibleBuilding(ai.Info.BuildingCommonNames.Silo, buildableThings); if (silo != null && HasSufficientPowerForActor(silo)) { AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (silo)", queue.Actor.Owner, silo.Name); return(silo); } if (power != null && silo != null && !HasSufficientPowerForActor(silo)) { AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); return(power); } } // 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; } // 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.NavalProduction.Contains(name) && (waterState == Water.NotEnoughWater || !AIUtils.IsAreaAvailable <GivesBuildableArea>(ai.World, ai.Player, ai.Map, ai.Info.CheckForWaterRadius, ai.Info.WaterTerrainTypes))) { continue; } // Will this put us into low power? var actor = world.Map.Rules.Actors[name]; if (playerPower != null && (playerPower.ExcessPower < minimumExcessPower || !HasSufficientPowerForActor(actor))) { // Try building a power plant instead if (power != null && power.TraitInfos <PowerInfo>().Where(i => i.EnabledByDefault).Sum(pi => pi.Amount) > 0) { if (playerPower.PowerOutageRemainingTicks > 0) { AIUtils.BotDebug("{0} decided to build {1}: Priority override (is low power)", queue.Actor.Owner, power.Name); } else { AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); } return(power); } } // Lets build this AIUtils.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. // AIUtils.BotDebug("{0} couldn't decide what to build for queue {1}.", queue.Actor.Owner, queue.Info.Group); return(null); }