Skip to content

Commit

Permalink
Add MinelayerBotModule
Browse files Browse the repository at this point in the history
  • Loading branch information
dnqbob authored and Mailaender committed Aug 8, 2023
1 parent 3ab421c commit 2b0afd6
Show file tree
Hide file tree
Showing 2 changed files with 370 additions and 1 deletion.
361 changes: 361 additions & 0 deletions OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,361 @@
#region Copyright & License Information
/*
* Copyright 2007-2022 The OpenRA Developers (see AUTHORS)
* 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.Generic;
using System.Linq;
using OpenRA.Mods.Common;
using OpenRA.Mods.Common.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;

namespace OpenRA.Mods.Cnc.Traits
{
[Desc("Manages AI minelayer unit related with Minelayer traits.",
"When enemy damage AI's actors, the location of conflict will be recorded,",
"If a location is confirmed as can lay mine, it will add/merge to favorite location for usage later")]
public class MinelayerBotModuleInfo : ConditionalTraitInfo
{
[Desc("Enemy target types to ignore when add the minefield location to conflict location.")]
public readonly BitSet<TargetableType> IgnoredEnemyTargetTypes = default;

[Desc("Victim target types that considering conflict location as enemy location instead of victim location.")]
public readonly BitSet<TargetableType> UseEnemyLocationTargetTypes = default;

[Desc("Actor types that used for mine laying, must have Minelayer.")]
public readonly HashSet<string> Minelayers = default;

[Desc("Find this amount of suitable actors and lay mine to a location.")]
public readonly int MaxMinelayersPerAssign = 1;

[Desc("Scan suitable actors and target in this interval.")]
public readonly int ScanTick = 331;

[Desc("Minelayer radius.")]
public readonly int MineFieldRadius = 1;

[Desc("Minefield location is cancelled if those whose target type belong to allied nearby.")]
public readonly BitSet<TargetableType> AwayFromAlliedTargetTypes = default;

[Desc("Minefield location is cancelled if those whose target type belong to enemy nearby.")]
public readonly BitSet<TargetableType> AwayFromEnemyTargetTypes = default;

[Desc("Minefield location check distance to AwayFromAlliedTargettype and AwayFromEnemyTargettype.",
"In addition, if any emeny actor within this range and minefield location is not cancelled,",
"minelayer will try lay mines at the 3/4 path to minefield location")]
public readonly int AwayFromCellDistance = 9;

[Desc("Merge conflict point minefield position to a favorite minefield position if within this range and closest.",
"If favorite minefield positions is at the max of 5, we always merge it to closest regardless of this")]
public readonly int FavoritePositionDistance = 6;

public override object Create(ActorInitializer init) { return new MinelayerBotModule(init.Self, this); }
}

public class MinelayerBotModule : ConditionalTrait<MinelayerBotModuleInfo>, IBotTick, IBotRespondToAttack
{
const int MaxPositionCacheLength = 5;
const int RepeatedAltertTicks = 40;

readonly World world;
readonly Player player;
readonly Predicate<Actor> unitCannotBeOrdered;
readonly Predicate<Actor> unitCannotBeOrderedOrIsBusy;
readonly CPos?[] conflictPositionQueue;
readonly CPos?[] favoritePositions;

int minAssignRoleDelayTicks;
int conflictPositionLength;
int favoritePositionsLength;
int currentFavoritePositionIndex;
int alertedTicks;

PathFinder pathFinder;

public MinelayerBotModule(Actor self, MinelayerBotModuleInfo info)
: base(info)
{
world = self.World;
player = self.Owner;
unitCannotBeOrdered = a => a == null || a.IsDead || !a.IsInWorld || a.Owner != player;
unitCannotBeOrderedOrIsBusy = a => unitCannotBeOrdered(a) || (!a.IsIdle && !(a.CurrentActivity is FlyIdle));

Check failure on line 90 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Linux (.NET 6.0)

Use pattern matching

Check failure on line 90 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Linux (.NET 6.0)

Use pattern matching

Check failure on line 90 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Windows (.NET 6.0)

Use pattern matching

Check failure on line 90 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Windows (.NET 6.0)

Use pattern matching
conflictPositionQueue = new CPos?[MaxPositionCacheLength] { null, null, null, null, null };
favoritePositions = new CPos?[MaxPositionCacheLength] { null, null, null, null, null };
}

protected override void TraitEnabled(Actor self)
{
// Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay.
minAssignRoleDelayTicks = world.LocalRandom.Next(0, Info.ScanTick);
alertedTicks = 0;
conflictPositionLength = 0;
favoritePositionsLength = 0;
currentFavoritePositionIndex = 0;
pathFinder = self.World.WorldActor.Trait<PathFinder>();
}

void IBotTick.BotTick(IBot bot)
{
if (alertedTicks > 0)
alertedTicks--;

if (--minAssignRoleDelayTicks <= 0)
{
minAssignRoleDelayTicks = Info.ScanTick;

var minelayingPosition = CPos.Zero;
var useFavoritePosition = false;
var layMineOnHalfway = false;

while (conflictPositionLength > 0)
{
minelayingPosition = conflictPositionQueue[0].Value;
var check = HasInvalidActorInCircle(world.Map.CenterOfCell(minelayingPosition), WDist.FromCells(Info.AwayFromCellDistance));

Check failure on line 122 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Linux (.NET 6.0)

Variable declaration can be deconstructed

Check failure on line 122 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Windows (.NET 6.0)

Variable declaration can be deconstructed

Check failure on line 122 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Windows (.NET 6.0)

Variable declaration can be deconstructed
if (check.hasInvalidActors)
DequeueFirstConflictPosition();
else
{
layMineOnHalfway = check.hasEnemyNearby;
break;
}
}

TraitPair<Minelayer>[] ats = null;

if (conflictPositionLength == 0)
{
// If enemy turtle themselves at base and we don't have valid position recorded,
// we will try find a location that at the middle of pathfinding cells
if (favoritePositionsLength == 0)
{
ats = world.ActorsWithTrait<Minelayer>().Where(at => !unitCannotBeOrderedOrIsBusy(at.Actor)).ToArray();
if (ats.Length == 0)
return;

var enemies = world.Actors.Where(a => IsPreferredEnemyUnit(a)).ToArray();
if (enemies.Length == 0)
return;

var enemy = enemies.Random(world.LocalRandom);

foreach (var at in ats)
{
var cells = pathFinder.FindPathToTargetCell(at.Actor, new[] { at.Actor.Location }, enemy.Location, BlockedByActor.Immovable, laneBias: false);
if (cells != null && !(cells.Count == 0))
{
AIUtils.BotDebug("AI ({0}): try find a location to lay mine.", player.ClientIndex);
EnqueueConflictPosition(cells[cells.Count / 2]);

// We don't do other things in this tick, just find new location and abort
return;
}
}

return;
}
else
{
while (favoritePositionsLength > 0)
{
minelayingPosition = favoritePositions[currentFavoritePositionIndex].Value;
var check = HasInvalidActorInCircle(world.Map.CenterOfCell(minelayingPosition), WDist.FromCells(Info.AwayFromCellDistance));

Check failure on line 170 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Linux (.NET 6.0)

Variable declaration can be deconstructed

Check failure on line 170 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Windows (.NET 6.0)

Variable declaration can be deconstructed
if (check.hasInvalidActors)
{
DeleteCurrentFavoritePosition();
if (favoritePositionsLength == 0)
return;
}
else
{
layMineOnHalfway = check.hasEnemyNearby;
useFavoritePosition = true;
break;
}
}
}
}

if (ats == null)

Check failure on line 187 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Linux (.NET 6.0)

Use compound assignment

Check failure on line 187 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Linux (.NET 6.0)

Use compound assignment

Check failure on line 187 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Windows (.NET 6.0)

Use compound assignment

Check failure on line 187 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Windows (.NET 6.0)

Use compound assignment
ats = world.ActorsWithTrait<Minelayer>().Where(at => !unitCannotBeOrderedOrIsBusy(at.Actor)).ToArray();

if (ats.Length == 0)
return;

var orderedActors = new List<Actor>();

foreach (var at in ats)
{
var cells = pathFinder.FindPathToTargetCell(at.Actor, new[] { at.Actor.Location }, minelayingPosition, BlockedByActor.Immovable, laneBias: false);
if (cells != null && !(cells.Count == 0))
{
orderedActors.Add(at.Actor);

// if there is enemy actor nearby, we will try to lay mine on
// 3/4 distance to desired position (the path cell is reversed)
if (layMineOnHalfway)
{
minelayingPosition = cells[cells.Count * 1 / 4];
layMineOnHalfway = false;
}

if (orderedActors.Count >= Info.MaxMinelayersPerAssign)
break;
}
}

if (orderedActors.Count > 0)
{
if (useFavoritePosition)
{
AIUtils.BotDebug("AI ({0}): Use favorite position {1} at index {2}", player.ClientIndex, minelayingPosition, currentFavoritePositionIndex);
NextFavoritePositionIndex();
}
else
{
DequeueFirstConflictPosition();
AddPositionToFavoritePositions(minelayingPosition);
AIUtils.BotDebug("AI ({0}): Use in time conflict position {1}", player.ClientIndex, minelayingPosition);
}

var vec = new CVec(Info.MineFieldRadius, Info.MineFieldRadius);
bot.QueueOrder(new Order("PlaceMinefield", null, Target.FromCell(world, minelayingPosition + vec), false, groupedActors: orderedActors.ToArray()) { ExtraLocation = minelayingPosition - vec });
bot.QueueOrder(new Order("Move", null, Target.FromCell(world, orderedActors.First().Location), true, groupedActors: orderedActors.ToArray()));
}
else
{
if (useFavoritePosition)
DeleteCurrentFavoritePosition();
else
DequeueFirstConflictPosition();
}
}
}

void DequeueFirstConflictPosition()
{
for (var i = 1; i < conflictPositionLength; i++)
conflictPositionQueue[i - 1] = conflictPositionQueue[i];
conflictPositionQueue[conflictPositionLength - 1] = null;
conflictPositionLength--;
}

void DeleteCurrentFavoritePosition()
{
for (var i = currentFavoritePositionIndex; i < favoritePositionsLength - 1; i++)
favoritePositions[i] = favoritePositions[i + 1];
favoritePositions[favoritePositionsLength - 1] = null;

if (--favoritePositionsLength > 0)
currentFavoritePositionIndex = currentFavoritePositionIndex % favoritePositionsLength;

Check failure on line 258 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Linux (.NET 6.0)

Use compound assignment

Check failure on line 258 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Linux (.NET 6.0)

Use compound assignment

Check failure on line 258 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Windows (.NET 6.0)

Use compound assignment
}

void AddPositionToFavoritePositions(CPos cpos)
{
var favoriteDistSquare = Info.FavoritePositionDistance * Info.FavoritePositionDistance;
var closestIndex = 0;
var closestDistSquare = int.MaxValue;
for (var i = 0; i < favoritePositionsLength; i++)
{
var lengthsquare = (favoritePositions[i].Value - cpos).LengthSquared;
if (lengthsquare < closestDistSquare)
{
closestIndex = i;
closestDistSquare = lengthsquare;
}
}

// Add new if there is space
if (closestDistSquare > favoriteDistSquare && favoritePositionsLength < favoritePositions.Length)
{
favoritePositions[favoritePositionsLength] = cpos;
favoritePositionsLength++;
}
else
{
var pos = favoritePositions[closestIndex].Value;
favoritePositions[closestIndex] = (pos - cpos) / 2 + cpos;
}
}

void NextFavoritePositionIndex()
{
currentFavoritePositionIndex = (currentFavoritePositionIndex + 1) % favoritePositionsLength;
}

bool IsPreferredEnemyUnit(Actor a)
{
if (a == null || a.IsDead || player.RelationshipWith(a.Owner) != PlayerRelationship.Enemy || a.Info.HasTraitInfo<HuskInfo>())
return false;

var targetTypes = a.GetEnabledTargetTypes();
return !targetTypes.IsEmpty && !targetTypes.Overlaps(Info.IgnoredEnemyTargetTypes);
}

(bool hasInvalidActors, bool hasEnemyNearby) HasInvalidActorInCircle(WPos pos, WDist dist)

Check failure on line 303 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Linux (.NET 6.0)

Tuple element names should use correct casing

Check failure on line 303 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Linux (.NET 6.0)

Tuple element names should use correct casing

Check failure on line 303 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Windows (.NET 6.0)

Tuple element names should use correct casing

Check failure on line 303 in OpenRA.Mods.Cnc/Traits/Player/BotModules/MinelayerBotModule.cs

View workflow job for this annotation

GitHub Actions / Windows (.NET 6.0)

Tuple element names should use correct casing
{
var hasInvalidActor = false;
var hasEnemyActor = false;
hasInvalidActor = world.FindActorsInCircle(pos, dist).Any(a =>
{
if (a.Owner.RelationshipWith(player) == PlayerRelationship.Ally)
{
var targetTypes = a.GetEnabledTargetTypes();
return !targetTypes.IsEmpty && targetTypes.Overlaps(Info.AwayFromAlliedTargetTypes);
}
if (a.Owner.RelationshipWith(player) == PlayerRelationship.Enemy)
{
hasEnemyActor = true;
var targetTypes = a.GetEnabledTargetTypes();
return !targetTypes.IsEmpty && targetTypes.Overlaps(Info.AwayFromEnemyTargetTypes);
}
return false;
});

return (hasInvalidActor, hasEnemyActor);
}

void EnqueueConflictPosition(CPos cPos)
{
if (conflictPositionLength < MaxPositionCacheLength)
{
conflictPositionQueue[conflictPositionLength] = cPos;
conflictPositionLength++;
}
else
conflictPositionQueue[MaxPositionCacheLength - 1] = cPos;
}

void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e)
{
if (alertedTicks > 0 || !IsPreferredEnemyUnit(e.Attacker))
return;

alertedTicks = RepeatedAltertTicks;

var hasInvalidActor = HasInvalidActorInCircle(self.CenterPosition, WDist.FromCells(Info.AwayFromCellDistance)).hasInvalidActors;

if (hasInvalidActor)
return;

var targetTypes = self.GetEnabledTargetTypes();
CPos pos;
if (!targetTypes.IsEmpty && targetTypes.Overlaps(Info.UseEnemyLocationTargetTypes))
pos = e.Attacker.Location;
else
pos = self.Location;

EnqueueConflictPosition(pos);
}
}
}
10 changes: 9 additions & 1 deletion mods/ra/rules/ai.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ Player:
RequiresCondition: enable-turtle-ai
SquadSize: 10
NavalUnitsTypes: ss, msub, dd, ca, lst, pt
ExcludeFromSquadsTypes: harv, mcv, dog, badr.bomber, u2
ExcludeFromSquadsTypes: harv, mcv, dog, badr.bomber, u2, mnly
ConstructionYardTypes: fact
NavalProductionTypes: spen, syrd
AirUnitsTypes: mig, yak, heli, hind, mh60
Expand Down Expand Up @@ -400,11 +400,19 @@ Player:
dd: 10
ca: 10
pt: 10
mnly: 2
UnitLimits:
dog: 4
harv: 8
jeep: 4
ftrk: 4
mnly: 2
MinelayerBotModule@turtle:
Minelayers: mnly
IgnoredEnemyTargetTypes: Building, Defence, Air
UseEnemyLocationTargetTypes: Building, Defence
AwayFromAlliedTargetTypes: Building, Defence
AwayFromEnemyTargetTypes: Building, Defence
SquadManagerBotModule@naval:
RequiresCondition: enable-naval-ai
SquadSize: 1
Expand Down

0 comments on commit 2b0afd6

Please sign in to comment.