Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
826 lines
31 KiB
C#
826 lines
31 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Frozen;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Traits
|
|
{
|
|
public enum BotMcvExpansionMode { CheckResource, CheckBase, CheckCurrentLocation }
|
|
|
|
[TraitLocation(SystemActors.Player)]
|
|
[Desc("Manages AI MCVs and expansion.")]
|
|
public class McvExpansionManagerBotModuleInfo : ConditionalTraitInfo, Requires<ResourceMapBotModuleInfo>, NotBefore<ResourceMapBotModuleInfo>
|
|
{
|
|
[Desc("Actor types that are considered MCVs (deploy into base builders).")]
|
|
public readonly FrozenSet<string> McvTypes = FrozenSet<string>.Empty;
|
|
|
|
[Desc("Actor types that are considered construction yards (base builders).")]
|
|
public readonly FrozenSet<string> ConstructionYardTypes = FrozenSet<string>.Empty;
|
|
|
|
[Desc("Actor types that are able to produce MCVs.")]
|
|
public readonly FrozenSet<string> McvFactoryTypes = FrozenSet<string>.Empty;
|
|
|
|
[Desc("Try to maintain at least this many ConstructionYardTypes, build an MCV if number is below this.")]
|
|
public readonly int MinimumConstructionYardCount = 1;
|
|
|
|
[Desc("Try to maintain at additional this many ConstructionYardTypes.")]
|
|
public readonly int AdditionalConstructionYardCount = 0;
|
|
|
|
[Desc("Build additional MCV if cash is above this.")]
|
|
public readonly int BuildAdditionalMCVCashAmount = 5000;
|
|
|
|
[Desc("Delay (in ticks) for giving orders to idle MCVs.")]
|
|
public readonly int ScanForNewMcvInterval = 20;
|
|
|
|
[Desc("Delay (in ticks) for checking and building a MCV.")]
|
|
public readonly int BuildMcvInterval = 101;
|
|
|
|
[Desc("Delay (in ticks) for moving a conyard to better expansion. Only work with more than 1 conyard.")]
|
|
public readonly int MoveConyardTick = 5700;
|
|
|
|
[Desc("Should moving the oldest or newest conyard be preferred? Random ordering if unset.")]
|
|
public readonly bool? MoveOldConyardFirst = null;
|
|
|
|
[Desc("Initial expansion mode chosen by AI.")]
|
|
public readonly BotMcvExpansionMode InitialExpansionMode = BotMcvExpansionMode.CheckResource;
|
|
|
|
[Desc("Allow the bot to switch expansion mode automatically on enough failure or successful attempts.")]
|
|
public readonly bool ExpansionModeAutoSwitch = true;
|
|
|
|
/* those are CheckResource mode options */
|
|
[Desc("Minimum distance (in cells) from the found resource creator location when checking for MCV deployment location.")]
|
|
public readonly int CRmodeMinDeployRadius = 2;
|
|
|
|
[Desc("Maximum distance (in cells) the found resource creator location when checking for MCV deployment location.")]
|
|
public readonly int CRmodeMaxDeployRadius = 20;
|
|
|
|
[Desc("When moving to a resource, what distance (in cells) to resource should we attempt to maintain?")]
|
|
public readonly int CRmodeTryMaintainRange = 8;
|
|
|
|
[Desc("Distance (in cells) to avoid a friendly conyard when choosing an expansion location.",
|
|
"Recommended to set it equal or larger than ResourceMapStrideRadius.")]
|
|
public readonly int CRmodeFriendlyConyardDislikeRange = 14;
|
|
|
|
[Desc("Distance (in cells) to avoid a friendly refinery when choosing an expansion location.",
|
|
"Recommended to set it equal or larger than ResourceMapStrideRadius.")]
|
|
public readonly int CRmodeFriendlyRefineryDislikeRange = 14;
|
|
|
|
/* those are CheckBase mode options */
|
|
[Desc("Minimum distance (in cells) from center of the base expansion when checking for MCV deployment location.")]
|
|
public readonly int CBmodeMinDeployRadius = 2;
|
|
|
|
[Desc("Maximum distance (in cells) from center of the base expansion when checking for MCV deployment location.")]
|
|
public readonly int CBmodeMaxDeployRadius = 20;
|
|
|
|
public override object Create(ActorInitializer init) { return new McvExpansionManagerBotModule(init.Self, this); }
|
|
}
|
|
|
|
public class McvExpansionManagerBotModule :
|
|
ConditionalTrait<McvExpansionManagerBotModuleInfo>,
|
|
IBotTick,
|
|
IBotRespondToAttack,
|
|
IBotBaseExpansion,
|
|
INotifyActorDisposing
|
|
{
|
|
// When ExpansionModeAutoSwitch is true, if the AI fails to find a deploy spot enough time even in CheckBase mode
|
|
// NegativeMaxFailedAttempts is applied to make AI switch bettween modes more frequently until a successful attempt
|
|
const int CRmodPositiveMaxFailedAttempts = 3;
|
|
const int CBmodPositiveMaxFailedAttempts = 2;
|
|
const int NegativeMaxFailedAttempts = 0;
|
|
|
|
readonly World world;
|
|
readonly Player player;
|
|
readonly ActorIndex.OwnerAndNamesAndTrait<TransformsInfo> mcvs;
|
|
readonly ActorIndex.OwnerAndNamesAndTrait<BuildingInfo> constructionYards;
|
|
readonly ActorIndex.OwnerAndNamesAndTrait<BuildingInfo> mcvFactories;
|
|
|
|
IBotPositionsUpdated[] notifyPositionsUpdated;
|
|
IBotRequestUnitProduction[] requestUnitProduction;
|
|
IBotSuggestRefineryProduction[] suggestRefineryProduction;
|
|
|
|
readonly Dictionary<Actor, CPos?> activeMCVs = [];
|
|
|
|
PathFinder pathfinder;
|
|
ResourceMapBotModule resourceMapModule;
|
|
PlayerResources playerResources;
|
|
Actor mustUndeployCoyard;
|
|
|
|
int scanInterval;
|
|
int buildMCVInterval;
|
|
int moveConyardInterval;
|
|
bool firstTick = true;
|
|
bool undeployEvenNoBase = false;
|
|
bool allowfallback = true;
|
|
|
|
BotMcvExpansionMode mcvExpansionMode;
|
|
int mcvDeploymentMinDeployRadius;
|
|
int mcvDeploymentMaxDeployRadius;
|
|
int mcvDeploymentTryMaintainRange;
|
|
int maxFailedAttempts;
|
|
|
|
int failedAttempts;
|
|
CPos? lastFailedCheckSpot;
|
|
|
|
// It is unnecessary to respond every tick, we only need to respond once in a while.
|
|
int attackrespondcooldown = 20;
|
|
|
|
int pathDistanceSquareFactor;
|
|
|
|
public McvExpansionManagerBotModule(Actor self, McvExpansionManagerBotModuleInfo info)
|
|
: base(info)
|
|
{
|
|
world = self.World;
|
|
player = self.Owner;
|
|
mcvs = new ActorIndex.OwnerAndNamesAndTrait<TransformsInfo>(world, info.McvTypes, player);
|
|
constructionYards = new ActorIndex.OwnerAndNamesAndTrait<BuildingInfo>(world, info.ConstructionYardTypes, player);
|
|
mcvFactories = new ActorIndex.OwnerAndNamesAndTrait<BuildingInfo>(world, info.McvFactoryTypes, player);
|
|
}
|
|
|
|
protected override void Created(Actor self)
|
|
{
|
|
// Special case handling is required for the Player actor.
|
|
// Created is called before Player.PlayerActor is assigned,
|
|
// so we must query player traits from self, which refers
|
|
// for bot modules always to the Player actor.
|
|
notifyPositionsUpdated = self.TraitsImplementing<IBotPositionsUpdated>().ToArray();
|
|
requestUnitProduction = self.TraitsImplementing<IBotRequestUnitProduction>().ToArray();
|
|
suggestRefineryProduction = self.TraitsImplementing<IBotSuggestRefineryProduction>().ToArray();
|
|
pathfinder = world.WorldActor.Trait<PathFinder>();
|
|
playerResources = self.Owner.PlayerActor.Trait<PlayerResources>();
|
|
}
|
|
|
|
protected override void TraitEnabled(Actor self)
|
|
{
|
|
// Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay.
|
|
scanInterval = world.LocalRandom.Next(Info.ScanForNewMcvInterval, Info.ScanForNewMcvInterval << 1);
|
|
buildMCVInterval = world.LocalRandom.Next(Info.BuildMcvInterval, Info.BuildMcvInterval << 1);
|
|
moveConyardInterval = world.LocalRandom.Next(Info.MoveConyardTick, Info.MoveConyardTick << 1);
|
|
}
|
|
|
|
void SwitchExpansionMode(BotMcvExpansionMode nextMode)
|
|
{
|
|
mcvExpansionMode = nextMode;
|
|
switch (nextMode)
|
|
{
|
|
case BotMcvExpansionMode.CheckResource:
|
|
mcvDeploymentMinDeployRadius = Info.CRmodeMinDeployRadius;
|
|
mcvDeploymentMaxDeployRadius = Info.CRmodeMaxDeployRadius;
|
|
mcvDeploymentTryMaintainRange = Info.CRmodeTryMaintainRange;
|
|
break;
|
|
|
|
case BotMcvExpansionMode.CheckBase:
|
|
mcvDeploymentMinDeployRadius = Info.CBmodeMinDeployRadius;
|
|
mcvDeploymentMaxDeployRadius = Info.CBmodeMaxDeployRadius;
|
|
mcvDeploymentTryMaintainRange = (Info.CBmodeMaxDeployRadius + Info.CBmodeMinDeployRadius) >> 1;
|
|
break;
|
|
|
|
case BotMcvExpansionMode.CheckCurrentLocation:
|
|
mcvDeploymentMinDeployRadius = Info.CBmodeMinDeployRadius;
|
|
mcvDeploymentMaxDeployRadius = Info.CBmodeMaxDeployRadius;
|
|
mcvDeploymentTryMaintainRange = 0;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FindBadDeploySpot(CPos? failedSpot)
|
|
{
|
|
lastFailedCheckSpot = failedSpot;
|
|
|
|
if (!Info.ExpansionModeAutoSwitch)
|
|
{
|
|
if (++failedAttempts >= maxFailedAttempts)
|
|
failedAttempts = maxFailedAttempts;
|
|
return;
|
|
}
|
|
|
|
if (++failedAttempts >= maxFailedAttempts)
|
|
{
|
|
failedAttempts = 0;
|
|
switch (mcvExpansionMode)
|
|
{
|
|
case BotMcvExpansionMode.CheckResource:
|
|
SwitchExpansionMode(BotMcvExpansionMode.CheckBase);
|
|
break;
|
|
|
|
case BotMcvExpansionMode.CheckBase:
|
|
SwitchExpansionMode(BotMcvExpansionMode.CheckResource);
|
|
maxFailedAttempts = NegativeMaxFailedAttempts;
|
|
break;
|
|
|
|
case BotMcvExpansionMode.CheckCurrentLocation:
|
|
SwitchExpansionMode(BotMcvExpansionMode.CheckResource);
|
|
maxFailedAttempts = NegativeMaxFailedAttempts;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FindGoodDeploySpot()
|
|
{
|
|
lastFailedCheckSpot = null;
|
|
|
|
if (!Info.ExpansionModeAutoSwitch)
|
|
{
|
|
if (--failedAttempts <= -maxFailedAttempts)
|
|
failedAttempts = -maxFailedAttempts;
|
|
return;
|
|
}
|
|
|
|
if (--failedAttempts <= -maxFailedAttempts)
|
|
{
|
|
switch (mcvExpansionMode)
|
|
{
|
|
case BotMcvExpansionMode.CheckResource:
|
|
maxFailedAttempts = CRmodPositiveMaxFailedAttempts;
|
|
failedAttempts = -maxFailedAttempts;
|
|
break;
|
|
|
|
case BotMcvExpansionMode.CheckBase:
|
|
maxFailedAttempts = CRmodPositiveMaxFailedAttempts;
|
|
failedAttempts = maxFailedAttempts - 1;
|
|
SwitchExpansionMode(BotMcvExpansionMode.CheckResource);
|
|
break;
|
|
|
|
case BotMcvExpansionMode.CheckCurrentLocation:
|
|
maxFailedAttempts = CBmodPositiveMaxFailedAttempts;
|
|
failedAttempts = maxFailedAttempts - 1;
|
|
SwitchExpansionMode(BotMcvExpansionMode.CheckBase);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public (CPos? ExpandLocation, int Attraction, CPos? CheckSpot) GetExpansionCenter(Actor mcv, Mobile mobile, bool allowfallback)
|
|
{
|
|
/*
|
|
* indiceSideLengthSquare (which is equal to indiceSideLength * indiceSideLength) is used as the basic unit to calculate the attraction of a candidate,
|
|
* we compare the attraction on the same scale on different factors, such as candidate's distance to current MCV and ally construction yard & refinery within range, etc:
|
|
*
|
|
* 1). the weight of candidate's distance-square to current MCV
|
|
*
|
|
* a) if not Mobile: range from 0 to -indiceSideLengthSquare.
|
|
*
|
|
* The reason why:
|
|
*
|
|
* It is calculated as "(candidate - mcv.Location).LengthSquared / pathDistanceSquareFactor".
|
|
* note that: pathDistanceSquareFactor = resourceMapIndicesColumnCount * resourceMapIndicesColumnCount + resourceMapIndicesRowCount * resourceMapIndicesRowCount,
|
|
*
|
|
* Consider a map, we divide it at the length of indiceSideLength = r, and then its resourceMapIndicesColumnCount = a, resourceMapIndicesRowCount = b,
|
|
* so the map.width ≈ a*r, map.height ≈ b*r,
|
|
* the maximum euclid distance-square between two points on the map is (a*r)(a*r) + (b*r)(b*r),
|
|
* so the maximum "weight of candidate's distance to current MCV" is from 0 to -((a*r)(a*r) + (b*r)(b*r)) / (a*a + b*b) = -r*r = -indiceSideLengthSquare.
|
|
*
|
|
* b) if Mobile: range depends on pathfinding distance in cell.
|
|
*
|
|
* It is calculated as "pathfindDistance * pathfindDistance / pathDistanceSquareFactor".
|
|
*
|
|
* 2). the weight of friendly construction yard within range: -indiceSideLengthSquare. If it belongs to an ally, -indiceSideLengthSquare/2.
|
|
*
|
|
* 3). the weight of enemy within range: -indiceSideLengthSquare*8 for base building, otherwise -indiceSideLengthSquare/64
|
|
*
|
|
* 4). the weight of friendly refinery within range (not for CheckBase mode): -indiceSideLengthSquare. If it belongs to an ally, -indiceSideLengthSquare/2.
|
|
*
|
|
* 5). the weight of resource amount (only for CheckResource mode): from 0 to +indiceSideLengthSquare/8.
|
|
*
|
|
* The reason why:
|
|
*
|
|
* The maximum resource amount in a indice of resource map is approximately indiceSideLengthSquare (full of it), but a stride full of resources is less likely to
|
|
* have room for buildings. So we prefer the indice have half of resource cells the most, which may give us enough room to place buildings.
|
|
*
|
|
* so the weight can be: (indiceSideLengthSquare/2) - |(indiceResourceCellCount - (indiceSideLengthSquare/2))|, range from (0 to +indiceSideLengthSquare/2).
|
|
*
|
|
* Note: In practive resource weight is not very important, we cannot let MCV go a long way just for a rich resource spot.
|
|
* We have to take only 1/4 of it, wich is (0 to +indiceSideLengthSquare/8),
|
|
* and apply some additional method to filter the indice for acceptable resource (not too low).
|
|
*/
|
|
var indiceSideLengthSquare = resourceMapModule.GetIndiceSideLength() * resourceMapModule.GetIndiceSideLength();
|
|
switch (mcvExpansionMode)
|
|
{
|
|
/*
|
|
* CheckBase mode only considers the distance to current MCV, ally construction yard within range and enemy buildings within range.
|
|
* Attaction has a base value of indiceSideLengthSquare >> 1 (1/2 of the maximum distance weight, 1/ sqrt(2) ≈ 1/1.4 of maximum euclid distance in map)
|
|
*/
|
|
case BotMcvExpansionMode.CheckBase:
|
|
var cb_conyardlocs = world.ActorsHavingTrait<Building>()
|
|
.Where(a => a.Owner.IsAlliedWith(player) && Info.ConstructionYardTypes.Contains(a.Info.Name))
|
|
.Select(a => (a.Location, a.Owner == player))
|
|
.ToArray();
|
|
CPos? cb_suitablespot = null;
|
|
CPos? cb_checkspot = null;
|
|
var cb_best = int.MinValue;
|
|
|
|
for (var i = 0; i < resourceMapModule.GetIndicesLength(); i++)
|
|
{
|
|
var indiceCenter = resourceMapModule.GetIndice(i).IndiceCenter;
|
|
|
|
if (lastFailedCheckSpot == indiceCenter)
|
|
continue;
|
|
|
|
var attraction = indiceSideLengthSquare >> 1;
|
|
|
|
attraction -= (indiceCenter - mcv.Location).LengthSquared / pathDistanceSquareFactor;
|
|
|
|
attraction -= CalculateThreats(indiceSideLengthSquare, i);
|
|
|
|
foreach (var (location, isAlly) in cb_conyardlocs)
|
|
{
|
|
var sdistance = (indiceCenter - location).LengthSquared;
|
|
if (sdistance <= indiceSideLengthSquare)
|
|
{
|
|
if (isAlly)
|
|
attraction -= indiceSideLengthSquare;
|
|
else
|
|
attraction -= indiceSideLengthSquare << 1;
|
|
}
|
|
}
|
|
|
|
foreach (var (othermcv, dest) in activeMCVs)
|
|
{
|
|
if (dest == indiceCenter && othermcv != mcv)
|
|
attraction -= indiceSideLengthSquare << 1;
|
|
}
|
|
|
|
if (!allowfallback)
|
|
{
|
|
var sdistance = (indiceCenter - mcv.Location).LengthSquared;
|
|
if (sdistance <= indiceSideLengthSquare)
|
|
attraction -= indiceSideLengthSquare << 1;
|
|
}
|
|
|
|
if (attraction > cb_best)
|
|
{
|
|
cb_best = attraction;
|
|
cb_checkspot = indiceCenter;
|
|
cb_suitablespot = indiceCenter;
|
|
}
|
|
}
|
|
|
|
return (cb_suitablespot ?? mcv.Location, cb_best, cb_checkspot);
|
|
|
|
/*
|
|
* CheckResource mode considers the distance to current MCV, ally construction yard & refinery within range,
|
|
* Attaction has a base value of:
|
|
* 1. if not Mobile: indiceSideLengthSquare >> 2 (1/4 of the maximum distance weight, = 0.5 of the maximum euclid distance in map)
|
|
* 2. if Mobile: indiceSideLengthSquare >> 1 (1/2 of the maximum distance weight, ≈ 0.71 of the maximum euclid distance in map)
|
|
*/
|
|
case BotMcvExpansionMode.CheckResource:
|
|
|
|
var cr_refinarylocs = world.ActorsHavingTrait<Refinery>()
|
|
.Where(a => a.Owner == player && resourceMapModule.Info.RefineryTypes.Contains(a.Info.Name))
|
|
.Select(a => (a.Location, a.Owner != player))
|
|
.ToArray();
|
|
|
|
var cr_conyardlocs = world.ActorsHavingTrait<Building>()
|
|
.Where(a => a.Owner.IsAlliedWith(player) && Info.ConstructionYardTypes.Contains(a.Info.Name))
|
|
.Select(a => (a.Location, a.Owner != player))
|
|
.ToArray();
|
|
|
|
// We only take indice has more than the half of average indice value (in weight calculation), to skip the indice with very poor resource
|
|
// when failedAttempts is acceptable.
|
|
var thresholdRes = 0;
|
|
for (var i = 0; i < resourceMapModule.GetIndicesLength(); i++)
|
|
{
|
|
var resourceCellCounts = resourceMapModule.GetIndice(i).ResourceCellsCount;
|
|
thresholdRes += (indiceSideLengthSquare >> 1) - Math.Abs(resourceCellCounts - (indiceSideLengthSquare >> 1));
|
|
}
|
|
|
|
thresholdRes = (thresholdRes / resourceMapModule.GetIndicesLength()) >> 1;
|
|
|
|
CPos? cr_suitablespot = null;
|
|
CPos? cr_checkspot = null;
|
|
var cr_best = int.MinValue;
|
|
|
|
for (var i = 0; i < resourceMapModule.GetIndicesLength(); i++)
|
|
{
|
|
var indice = resourceMapModule.GetIndice(i);
|
|
var indiceCenter = indice.IndiceCenter;
|
|
var resourceCellsCount = indice.ResourceCellsCount;
|
|
var resourceCellsCenter = indice.ResourceCellsCenter;
|
|
var resourceCreatorLocs = indice.ResourceCreatorLocs;
|
|
|
|
if ((failedAttempts > maxFailedAttempts >> 1 && resourceCellsCount <= thresholdRes) || lastFailedCheckSpot == indiceCenter)
|
|
continue;
|
|
|
|
var attraction = 0;
|
|
if (mobile == null)
|
|
{
|
|
attraction = indiceSideLengthSquare >> 2;
|
|
attraction -= (resourceCellsCenter - mcv.Location).LengthSquared / pathDistanceSquareFactor;
|
|
}
|
|
else
|
|
{
|
|
attraction = indiceSideLengthSquare >> 1;
|
|
|
|
var path = pathfinder.FindPathToTargetCells(mcv, mcv.Location, [resourceCellsCenter], BlockedByActor.None);
|
|
|
|
if (path == PathFinder.NoPath)
|
|
continue;
|
|
|
|
attraction -= path.Count * path.Count / pathDistanceSquareFactor;
|
|
}
|
|
|
|
// it is better that resource cells takes only half of the indice cells, which give us the place to place building.
|
|
attraction += ((indiceSideLengthSquare >> 1) - Math.Abs(resourceCellsCount - (indiceSideLengthSquare >> 1))) >> 2;
|
|
attraction += 8 * resourceCreatorLocs.Length;
|
|
|
|
var resCenter = resourceCreatorLocs.Length == 0 || world.LocalRandom.Next(2) > 0 ? resourceCellsCenter : resourceCreatorLocs.Random(world.LocalRandom);
|
|
|
|
attraction -= CalculateThreats(indiceSideLengthSquare, i);
|
|
|
|
foreach (var (location, isAlly) in cr_refinarylocs)
|
|
{
|
|
var sdistance = (resCenter - location).LengthSquared;
|
|
if (sdistance <= Info.CRmodeFriendlyRefineryDislikeRange * Info.CRmodeFriendlyRefineryDislikeRange)
|
|
{
|
|
if (isAlly)
|
|
attraction -= indiceSideLengthSquare;
|
|
else
|
|
attraction -= indiceSideLengthSquare << 1;
|
|
}
|
|
}
|
|
|
|
foreach (var (location, isAlly) in cr_conyardlocs)
|
|
{
|
|
var sdistance = (resCenter - location).LengthSquared;
|
|
if (sdistance <= Info.CRmodeFriendlyConyardDislikeRange * Info.CRmodeFriendlyConyardDislikeRange)
|
|
{
|
|
if (isAlly)
|
|
attraction -= indiceSideLengthSquare;
|
|
else
|
|
attraction -= indiceSideLengthSquare << 1;
|
|
}
|
|
}
|
|
|
|
foreach (var (othermcv, dest) in activeMCVs)
|
|
{
|
|
if (dest == indiceCenter && othermcv != mcv)
|
|
attraction -= indiceSideLengthSquare << 1;
|
|
}
|
|
|
|
if (!allowfallback)
|
|
{
|
|
var sdistance = (resCenter - mcv.Location).LengthSquared;
|
|
if (sdistance <= Info.CRmodeFriendlyConyardDislikeRange * Info.CRmodeFriendlyConyardDislikeRange)
|
|
attraction -= indiceSideLengthSquare << 1;
|
|
}
|
|
|
|
if (attraction > cr_best)
|
|
{
|
|
cr_best = attraction;
|
|
cr_checkspot = indiceCenter;
|
|
cr_suitablespot = resCenter;
|
|
}
|
|
}
|
|
|
|
if (cr_suitablespot == null)
|
|
return (null, int.MinValue, null);
|
|
|
|
return (cr_suitablespot, cr_best, cr_checkspot);
|
|
|
|
case BotMcvExpansionMode.CheckCurrentLocation:
|
|
return (mcv.Location, int.MaxValue, null);
|
|
|
|
default:
|
|
return (null, int.MinValue, null);
|
|
}
|
|
}
|
|
|
|
int CalculateThreats(int indiceSideLengthSquare, int index)
|
|
{
|
|
var baseIndice = resourceMapModule.GetIndice(index);
|
|
|
|
var (indiceCount, nearbyEnemyThreat, nearbyEnemyBaseThreat) = resourceMapModule.GetNearbyIndicesThreat(index);
|
|
|
|
var indiceEnemyBaseThreat = Math.Max(baseIndice.EnemyBaseCount - baseIndice.FriendlyBaseCount, 0);
|
|
|
|
var indiceEnemyUnitThreat = Math.Max(baseIndice.EnemyUnitCount - baseIndice.FriendlyUnitCount, 0);
|
|
|
|
if (indiceCount == 0)
|
|
return (indiceEnemyUnitThreat * indiceSideLengthSquare >> 6) + (indiceEnemyBaseThreat * indiceSideLengthSquare << 3);
|
|
|
|
return ((indiceEnemyUnitThreat * indiceSideLengthSquare + nearbyEnemyThreat * indiceSideLengthSquare / indiceCount) >> 6) +
|
|
((indiceEnemyBaseThreat * indiceSideLengthSquare + nearbyEnemyBaseThreat * indiceSideLengthSquare / indiceCount) << 3);
|
|
}
|
|
|
|
void IBotTick.BotTick(IBot bot)
|
|
{
|
|
attackrespondcooldown--;
|
|
|
|
if (firstTick)
|
|
{
|
|
resourceMapModule = bot.Player.PlayerActor.TraitsImplementing<ResourceMapBotModule>().FirstOrDefault(t => t.IsTraitEnabled());
|
|
SwitchExpansionMode(Info.InitialExpansionMode);
|
|
|
|
pathDistanceSquareFactor = resourceMapModule.GetIndiceRowCount() * resourceMapModule.GetIndiceRowCount()
|
|
+ resourceMapModule.GetIndiceColumnCount() * resourceMapModule.GetIndiceColumnCount();
|
|
|
|
DeployMcvs(bot, false);
|
|
firstTick = false;
|
|
}
|
|
|
|
if (--scanInterval <= 0)
|
|
{
|
|
foreach (var amcv in activeMCVs.Keys.ToList())
|
|
{
|
|
if (amcv.IsDead || !amcv.IsInWorld)
|
|
activeMCVs.Remove(amcv);
|
|
}
|
|
|
|
scanInterval = Info.ScanForNewMcvInterval;
|
|
DeployMcvs(bot, true);
|
|
}
|
|
|
|
if (--buildMCVInterval <= 0)
|
|
{
|
|
buildMCVInterval = Info.BuildMcvInterval;
|
|
BuildMCV(bot);
|
|
}
|
|
|
|
if (--moveConyardInterval <= 0)
|
|
{
|
|
foreach (var amcv in activeMCVs.Keys.ToList())
|
|
{
|
|
if (amcv.IsDead || !amcv.IsInWorld)
|
|
activeMCVs.Remove(amcv);
|
|
}
|
|
|
|
moveConyardInterval = Info.MoveConyardTick;
|
|
UnDeployConyard(bot);
|
|
}
|
|
}
|
|
|
|
void BuildMCV(IBot bot)
|
|
{
|
|
if (Info.McvTypes.Count <= 0)
|
|
return;
|
|
if (AIUtils.CountActorByCommonName(mcvFactories) <= 0)
|
|
return;
|
|
var mcvNum = AIUtils.CountActorByCommonName(mcvs);
|
|
var conyardNum = AIUtils.CountActorByCommonName(constructionYards);
|
|
|
|
var mcvShouldHave = playerResources.GetCashAndResources() >= Info.BuildAdditionalMCVCashAmount
|
|
? Info.MinimumConstructionYardCount + Info.AdditionalConstructionYardCount : Info.MinimumConstructionYardCount;
|
|
|
|
// If we only have 1 MCV and no conyard, we should be allowed to build another MCV.
|
|
// Otherwise, when an mcv is on the move and we should wait.
|
|
if ((conyardNum <= 0 && mcvNum > 1) || (conyardNum > 0 && mcvNum > 0))
|
|
return;
|
|
|
|
if (conyardNum + mcvNum >= mcvShouldHave)
|
|
return;
|
|
|
|
// We have MCV in production queue, let's wait.
|
|
if (mcvFactories.Actors
|
|
.Any(a => !a.IsDead && a.TraitsImplementing<ProductionQueue>().Any(t => t.Enabled && t.AllQueued().Any(q => Info.McvTypes.Contains(q.Item)))))
|
|
return;
|
|
|
|
// We have MCV in production queue, let's wait.
|
|
if (player.PlayerActor.TraitsImplementing<ProductionQueue>()
|
|
.Any(t => t.Enabled && t.AllQueued().Any(q => Info.McvTypes.Contains(q.Item))))
|
|
return;
|
|
var unitBuilder = requestUnitProduction.FirstEnabledTraitOrDefault();
|
|
if (unitBuilder == null)
|
|
return;
|
|
var mcvType = Info.McvTypes.Random(world.LocalRandom);
|
|
|
|
// Make sure we only request one MCV at a time.
|
|
if (unitBuilder.RequestedProductionCount(bot, mcvType) <= 0)
|
|
unitBuilder.RequestUnitProduction(bot, mcvType);
|
|
}
|
|
|
|
void DeployMcvs(IBot bot, bool chooseLocation)
|
|
{
|
|
var newMCVs = world.ActorsHavingTrait<Transforms>()
|
|
.Where(a => a.Owner == player && a.IsIdle && Info.McvTypes.Contains(a.Info.Name));
|
|
|
|
foreach (var mcv in newMCVs)
|
|
DeployMcv(bot, mcv, chooseLocation);
|
|
}
|
|
|
|
void UnDeployConyard(IBot bot)
|
|
{
|
|
if (mustUndeployCoyard != null && mustUndeployCoyard.IsInWorld && !mustUndeployCoyard.IsDead && mustUndeployCoyard.Owner == player)
|
|
{
|
|
bot.QueueOrder(new Order("DeployTransform", mustUndeployCoyard, true));
|
|
mustUndeployCoyard = null;
|
|
|
|
return;
|
|
}
|
|
|
|
if (activeMCVs.Count > 0)
|
|
return;
|
|
|
|
var conyards = constructionYards.Actors
|
|
.Where(a => !a.IsDead);
|
|
|
|
var moveOldConyardFirst = Info.MoveOldConyardFirst ?? world.LocalRandom.Next(2) > 0;
|
|
|
|
if (moveOldConyardFirst)
|
|
conyards = conyards.OrderBy(a => a.ActorID);
|
|
else
|
|
conyards = conyards.OrderByDescending(a => a.ActorID);
|
|
|
|
var conyardslist = conyards.ToList();
|
|
|
|
if (conyardslist.Count > 1 || undeployEvenNoBase)
|
|
{
|
|
// We don't want to interrupt refinery production, otherwise it may cause a dead loop of deploy/undeploy.
|
|
var movableMCV = conyardslist.FirstOrDefault(a => !a.TraitsImplementing<ProductionQueue>()
|
|
.Any(t => t.Enabled && t.AllQueued().Any(q => resourceMapModule.Info.RefineryTypes.Contains(q.Item))));
|
|
|
|
if (movableMCV != null)
|
|
bot.QueueOrder(new Order("DeployTransform", movableMCV, true));
|
|
|
|
undeployEvenNoBase = false;
|
|
}
|
|
}
|
|
|
|
// Find any MCV and deploy them at a sensible location.
|
|
void DeployMcv(IBot bot, Actor mcv, bool move)
|
|
{
|
|
CPos? desiredLocation = null;
|
|
var transformsInfo = mcv.Info.TraitInfo<TransformsInfo>();
|
|
var actorInfo = world.Map.Rules.Actors[transformsInfo.IntoActor];
|
|
var bi = actorInfo.TraitInfoOrDefault<BuildingInfo>();
|
|
if (bi == null)
|
|
return;
|
|
|
|
if (move)
|
|
{
|
|
var (deployLocation, resLoc, checkloc) = ChooseMcvDeployLocation(mcv, actorInfo, bi, transformsInfo.Offset, allowfallback);
|
|
allowfallback = true;
|
|
desiredLocation = deployLocation;
|
|
if (desiredLocation == null)
|
|
return;
|
|
|
|
activeMCVs[mcv] = checkloc;
|
|
if (resLoc != null)
|
|
{
|
|
foreach (var srp in suggestRefineryProduction)
|
|
srp.RequestLocation(resLoc.Value, desiredLocation.Value, mcv);
|
|
}
|
|
|
|
bot.QueueOrder(new Order("Move", mcv, Target.FromCell(world, desiredLocation.Value), true));
|
|
}
|
|
else
|
|
{
|
|
if (!world.CanPlaceBuilding(mcv.Location + transformsInfo.Offset, actorInfo, bi, mcv))
|
|
return;
|
|
desiredLocation = mcv.Location;
|
|
}
|
|
|
|
bot.QueueOrder(new Order("DeployTransform", mcv, true));
|
|
|
|
// When we don't have a construction yard, we notify the new location to other traits for defence,
|
|
// If not, we only notify sometimes, because we are not sure if mcv can successfully deploy at the desired location.
|
|
// TODO: This could be addressed via INotifyTransform.
|
|
if (constructionYards.Actors.All(a => a.IsDead) || world.LocalRandom.Next(2) > 0)
|
|
{
|
|
foreach (var n in notifyPositionsUpdated)
|
|
{
|
|
n.UpdatedBaseCenter(desiredLocation.Value);
|
|
n.UpdatedDefenseCenter(desiredLocation.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// First, find a suitable expansion location according to current mode,
|
|
// Then, find a deployable cell around it.
|
|
(CPos? DeployLoc, CPos? ResourceLoc, CPos? CheckLoc) ChooseMcvDeployLocation(
|
|
Actor mcv,
|
|
ActorInfo transformIntoInfo,
|
|
BuildingInfo transformIntoBuildingInfo,
|
|
CVec offset,
|
|
bool allowfallback)
|
|
{
|
|
if (!mcv.Info.HasTraitInfo<IMoveInfo>())
|
|
return (null, null, null);
|
|
|
|
var mobile = mcv.TraitOrDefault<Mobile>();
|
|
|
|
var (expandCenter, attraction, checkspot) = GetExpansionCenter(mcv, mobile, allowfallback);
|
|
|
|
// Find the deployable cell
|
|
CPos? FindDeployCell(CPos? sourceCell, CPos? targetCell, int minRange, int maxRange, int tryMaintainRange)
|
|
{
|
|
if (!sourceCell.HasValue || !targetCell.HasValue)
|
|
return null;
|
|
|
|
var target = targetCell.Value;
|
|
var source = sourceCell.Value;
|
|
|
|
var cells = world.Map.FindTilesInAnnulus(target, minRange, maxRange);
|
|
|
|
/* First, sort the cells that keep tryMaintainRange to target (meanwhile direction is from center to target) the first to be considered
|
|
* by using following code. The idea is to use a linear combination of two distances-square for sorting weight.
|
|
*
|
|
* See comments in https://github.com/OpenRA/OpenRA/pull/22028#issuecomment-3242518793 for explaination.
|
|
*/
|
|
if (source != target)
|
|
{
|
|
var theta = tryMaintainRange;
|
|
var deta = (target - source).Length - tryMaintainRange;
|
|
cells = cells.OrderBy(c => deta * (c - target).LengthSquared + theta * (c - source).LengthSquared);
|
|
}
|
|
else
|
|
cells = cells.Shuffle(world.LocalRandom);
|
|
|
|
CPos? bestcell = null;
|
|
foreach (var cell in cells)
|
|
{
|
|
if (world.CanPlaceBuilding(cell + offset, transformIntoInfo, transformIntoBuildingInfo, mcv))
|
|
{
|
|
bestcell = cell;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If no deployble cell found, return null
|
|
if (bestcell == null)
|
|
return null;
|
|
|
|
if (source != target && mobile != null && !pathfinder.PathMightExistForLocomotorBlockedByImmovable(mobile.Locomotor, source, bestcell.Value))
|
|
bestcell = null;
|
|
|
|
// If the best deploy cell is not ideal ( >= tryMaintainRange + 2), which means there might be some huge blockers
|
|
// so we fall back to default behavior, which is the directly closest cell to target
|
|
if (!bestcell.HasValue || (source != target && (bestcell.Value - target).LengthSquared >= (tryMaintainRange + 2) * (tryMaintainRange + 2)))
|
|
{
|
|
cells = cells.OrderBy(c => (c - target).LengthSquared);
|
|
foreach (var cell in cells)
|
|
{
|
|
if (world.CanPlaceBuilding(cell + offset, transformIntoInfo, transformIntoBuildingInfo, mcv))
|
|
{
|
|
if (mobile != null && !pathfinder.PathMightExistForLocomotorBlockedByImmovable(mobile.Locomotor, source, cell))
|
|
return null;
|
|
|
|
return (!bestcell.HasValue) || (cell - target).LengthSquared < (bestcell.Value - target).LengthSquared ? cell : bestcell;
|
|
}
|
|
}
|
|
}
|
|
|
|
return bestcell;
|
|
}
|
|
|
|
var bc = FindDeployCell(mcv.Location, expandCenter, mcvDeploymentMinDeployRadius, mcvDeploymentMaxDeployRadius, mcvDeploymentTryMaintainRange);
|
|
|
|
// At last, if the attraction of the found expansion location is good enough (>0) and deploy cell found,
|
|
// we consider it as a good expansion, otherwise, we consider it as a bad expansion.
|
|
if (bc.HasValue && attraction > 0)
|
|
FindGoodDeploySpot();
|
|
else
|
|
FindBadDeploySpot(bc.HasValue ? null : checkspot);
|
|
|
|
if (mcvExpansionMode == BotMcvExpansionMode.CheckResource && expandCenter.HasValue && bc.HasValue)
|
|
return (bc, expandCenter, checkspot);
|
|
|
|
return (bc, null, checkspot);
|
|
}
|
|
|
|
void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e)
|
|
{
|
|
if (attackrespondcooldown <= 0 && Info.McvTypes.Contains(self.Info.Name))
|
|
{
|
|
attackrespondcooldown = 20;
|
|
|
|
DeployMcv(bot, self, false);
|
|
|
|
if (AIUtils.CountActorByCommonName(constructionYards) == 0)
|
|
{
|
|
foreach (var n in notifyPositionsUpdated)
|
|
n.UpdatedBaseCenter(self.Location);
|
|
}
|
|
}
|
|
}
|
|
|
|
void INotifyActorDisposing.Disposing(Actor self)
|
|
{
|
|
mcvs.Dispose();
|
|
constructionYards.Dispose();
|
|
mcvFactories.Dispose();
|
|
}
|
|
|
|
void IBotBaseExpansion.UpdateExpansionParams(IBot bot, bool fallback, bool undeployEvenNoBase, Actor mustUndeploy)
|
|
{
|
|
moveConyardInterval = 20; // allow some order latency
|
|
allowfallback = fallback;
|
|
this.undeployEvenNoBase = undeployEvenNoBase;
|
|
}
|
|
}
|
|
}
|