diff --git a/OpenRA.Game/Traits/Player/PlayerResources.cs b/OpenRA.Game/Traits/Player/PlayerResources.cs index 9f0261e4d9..e3f45b16eb 100644 --- a/OpenRA.Game/Traits/Player/PlayerResources.cs +++ b/OpenRA.Game/Traits/Player/PlayerResources.cs @@ -85,6 +85,7 @@ namespace OpenRA.Traits public int DisplayCash; public int DisplayOre; + public bool AlertSilo; public int Earned; public int Spent; @@ -158,8 +159,13 @@ namespace OpenRA.Traits if (--nextSiloAdviceTime <= 0) { - if (Ore > 0.8*OreCapacity) + if (Ore > 0.8 * OreCapacity) + { Sound.PlayNotification(Owner, "Speech", "SilosNeeded", Owner.Country.Race); + AlertSilo = true; + } + else + AlertSilo = false; nextSiloAdviceTime = AdviceInterval; } diff --git a/OpenRA.Mods.RA/AI/AttackOrFleeFuzzy.cs b/OpenRA.Mods.RA/AI/AttackOrFleeFuzzy.cs new file mode 100644 index 0000000000..4aabf6d32f --- /dev/null +++ b/OpenRA.Mods.RA/AI/AttackOrFleeFuzzy.cs @@ -0,0 +1,250 @@ +#region Copyright & License Information +/* + * Copyright 2007-2012 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using OpenRA.Mods.RA.Move; +using OpenRA.Traits; +using AI.Fuzzy.Library; + +namespace OpenRA.Mods.RA.AI +{ + class AttackOrFleeFuzzy + { + protected MamdaniFuzzySystem fuzzyEngine; + protected Dictionary result; + + public bool CanAttack + { + get + { + //not sure that this will happen (NaN), it's for the safety of + if (result[fuzzyEngine.OutputByName("AttackOrFlee")].ToString() != "NaN") + return result[fuzzyEngine.OutputByName("AttackOrFlee")] < 30.0; + return false; + } + } + + public AttackOrFleeFuzzy() + { + InitializateFuzzyVariables(); + } + + protected void InitializateFuzzyVariables() + { + fuzzyEngine = new MamdaniFuzzySystem(); + + FuzzyVariable playerHealthFuzzy = new FuzzyVariable("OwnHealth", 0.0, 100.0); + playerHealthFuzzy.Terms.Add(new FuzzyTerm("NearDead", new TrapezoidMembershipFunction(0, 0, 20, 40))); + playerHealthFuzzy.Terms.Add(new FuzzyTerm("Injured", new TrapezoidMembershipFunction(30, 50, 50, 70))); + playerHealthFuzzy.Terms.Add(new FuzzyTerm("Normal", new TrapezoidMembershipFunction(50, 80, 100, 100))); + fuzzyEngine.Input.Add(playerHealthFuzzy); + + FuzzyVariable enemyHealthFuzzy = new FuzzyVariable("EnemyHealth", 0.0, 100.0); + enemyHealthFuzzy.Terms.Add(new FuzzyTerm("NearDead", new TrapezoidMembershipFunction(0, 0, 20, 40))); + enemyHealthFuzzy.Terms.Add(new FuzzyTerm("Injured", new TrapezoidMembershipFunction(30, 50, 50, 70))); + enemyHealthFuzzy.Terms.Add(new FuzzyTerm("Normal", new TrapezoidMembershipFunction(50, 80, 100, 100))); + fuzzyEngine.Input.Add(enemyHealthFuzzy); + + FuzzyVariable relativeAttackPowerFuzzy = new FuzzyVariable("RelativeAttackPower", 0.0, 1000.0); + relativeAttackPowerFuzzy.Terms.Add(new FuzzyTerm("Weak", new TrapezoidMembershipFunction(0, 0, 70, 90))); + relativeAttackPowerFuzzy.Terms.Add(new FuzzyTerm("Equal", new TrapezoidMembershipFunction(85, 100, 100, 115))); + relativeAttackPowerFuzzy.Terms.Add(new FuzzyTerm("Strong", new TrapezoidMembershipFunction(110, 150, 150, 1000))); + fuzzyEngine.Input.Add(relativeAttackPowerFuzzy); + + FuzzyVariable relativeSpeedFuzzy = new FuzzyVariable("RelativeSpeed", 0.0, 1000.0); + relativeSpeedFuzzy.Terms.Add(new FuzzyTerm("Slow", new TrapezoidMembershipFunction(0, 0, 70, 90))); + relativeSpeedFuzzy.Terms.Add(new FuzzyTerm("Equal", new TrapezoidMembershipFunction(85, 100, 100, 115))); + relativeSpeedFuzzy.Terms.Add(new FuzzyTerm("Fast", new TrapezoidMembershipFunction(110, 150, 150, 1000))); + fuzzyEngine.Input.Add(relativeSpeedFuzzy); + + FuzzyVariable attackOrFleeFuzzy = new FuzzyVariable("AttackOrFlee", 0.0, 50.0); + attackOrFleeFuzzy.Terms.Add(new FuzzyTerm("Attack", new TrapezoidMembershipFunction(0, 15, 15, 30))); + attackOrFleeFuzzy.Terms.Add(new FuzzyTerm("Flee", new TrapezoidMembershipFunction(25, 35, 35, 50))); + fuzzyEngine.Output.Add(attackOrFleeFuzzy); + + AddingRulesForNormalOwnHealth(); + AddingRulesForInjuredOwnHealth(); + AddingRulesForNearDeadOwnHealth(); + } + + protected virtual void AddingRulesForNormalOwnHealth() + { + //1 OwnHealth is Normal + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if ((OwnHealth is Normal) " + + "and ((EnemyHealth is NearDead) or (EnemyHealth is Injured) or (EnemyHealth is Normal)) " + + "and ((RelativeAttackPower is Weak) or (RelativeAttackPower is Equal) or (RelativeAttackPower is Strong)) " + + "and ((RelativeSpeed is Slow) or (RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " + + "then AttackOrFlee is Attack")); + } + + protected virtual void AddingRulesForInjuredOwnHealth() + { + //OwnHealth is Injured + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if ((OwnHealth is Injured) " + + "and (EnemyHealth is NearDead) " + + "and ((RelativeAttackPower is Weak) or (RelativeAttackPower is Equal) or (RelativeAttackPower is Strong)) " + + "and ((RelativeSpeed is Slow) or (RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " + + "then AttackOrFlee is Attack")); + + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if ((OwnHealth is Injured) " + + "and ((EnemyHealth is Injured) or (EnemyHealth is Normal)) " + + "and ((RelativeAttackPower is Equal) or (RelativeAttackPower is Strong)) " + + "and ((RelativeSpeed is Slow) or (RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " + + "then AttackOrFlee is Attack")); + + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if ((OwnHealth is Injured) " + + "and ((EnemyHealth is Injured) or (EnemyHealth is Normal)) " + + "and (RelativeAttackPower is Weak) " + + "and (RelativeSpeed is Slow)) " + + "then AttackOrFlee is Attack")); + + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if ((OwnHealth is Injured) " + + "and ((EnemyHealth is Injured) or (EnemyHealth is Normal)) " + + "and (RelativeAttackPower is Weak) " + + "and ((RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " + + "then AttackOrFlee is Flee")); + + //2 + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if ((OwnHealth is Injured) " + + "and ((EnemyHealth is NearDead) or (EnemyHealth is Injured) or (EnemyHealth is Normal)) " + + "and ((RelativeAttackPower is Weak) or (RelativeAttackPower is Equal) or (RelativeAttackPower is Strong)) " + + "and (RelativeSpeed is Slow)) " + + "then AttackOrFlee is Attack")); + } + + protected virtual void AddingRulesForNearDeadOwnHealth() + { + //3 OwnHealth is NearDead + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if ((OwnHealth is NearDead) " + + "and (EnemyHealth is Injured) " + + "and (RelativeAttackPower is Equal) " + + "and ((RelativeSpeed is Slow) or (RelativeSpeed is Equal))) " + + "then AttackOrFlee is Attack")); + //4 + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if ((OwnHealth is NearDead) " + + "and (EnemyHealth is NearDead) " + + "and (RelativeAttackPower is Weak) " + + "and ((RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " + + "then AttackOrFlee is Flee")); + //5 + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if ((OwnHealth is NearDead) " + + "and (EnemyHealth is Injured) " + + "and (RelativeAttackPower is Weak) " + + "and ((RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " + + "then AttackOrFlee is Flee")); + + //6 + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if ((OwnHealth is NearDead) " + + "and (EnemyHealth is Normal) " + + "and (RelativeAttackPower is Weak) " + + "and ((RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " + + "then AttackOrFlee is Flee")); + + //7 + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if (OwnHealth is NearDead) " + + "and (EnemyHealth is Normal) " + + "and (RelativeAttackPower is Equal) " + + "and (RelativeSpeed is Fast) " + + "then AttackOrFlee is Flee")); + //8 + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if (OwnHealth is NearDead) " + + "and (EnemyHealth is Normal) " + + "and (RelativeAttackPower is Strong) " + + "and (RelativeSpeed is Fast) " + + "then AttackOrFlee is Flee")); + + //9 + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if (OwnHealth is NearDead) " + + "and (EnemyHealth is Injured) " + + "and (RelativeAttackPower is Equal) " + + "and (RelativeSpeed is Fast) " + + "then AttackOrFlee is Flee")); + } + public void CalculateFuzzy(List ownUnits, List enemyUnits) + { + Dictionary inputValues = new Dictionary(); + inputValues.Add(fuzzyEngine.InputByName("OwnHealth"), (double)NormalizedHealth(ownUnits, 100)); + inputValues.Add(fuzzyEngine.InputByName("EnemyHealth"), (double)NormalizedHealth(enemyUnits, 100)); + inputValues.Add(fuzzyEngine.InputByName("RelativeAttackPower"), (double)RelativePower(ownUnits, enemyUnits)); + inputValues.Add(fuzzyEngine.InputByName("RelativeSpeed"), (double)RelativeSpeed(ownUnits, enemyUnits)); + + result = fuzzyEngine.Calculate(inputValues); + } + + protected float NormalizedHealth(List actors, float normalizeByValue) + { + int sumOfMaxHp = 0; + int sumOfHp = 0; + foreach (var a in actors) + if (a.HasTrait()) + { + sumOfMaxHp += a.Trait().MaxHP; + sumOfHp += a.Trait().HP; + } + if (sumOfMaxHp == 0) return 0.0f; + return (sumOfHp * normalizeByValue) / sumOfMaxHp; + } + + protected float RelativePower(List own, List enemy) + { + return RelativeValue(own, enemy, 100, SumOfValues, (Actor a) => + { + int sumOfDamage = 0; + foreach (var weap in a.Trait().Weapons) + if (weap.Info.Warheads[0] != null) + sumOfDamage += weap.Info.Warheads[0].Damage; + return sumOfDamage; + }); + } + + protected float RelativeSpeed(List own, List enemy) + { + return RelativeValue(own, enemy, 100, Average, (Actor a) => a.Trait().Info.Speed); + } + + protected float RelativeValue(List own, List enemy, float normalizeByValue, + Func, Func, float> relativeFunc, Func getValue) + { + if (enemy.Count == 0) + return 999.0f; + if (own.Count == 0) + return 0.0f; + + float relative = (relativeFunc(own, getValue) / relativeFunc(enemy, getValue)) * normalizeByValue; + return relative.Clamp(0.0f, 999.0f); + } + + protected float SumOfValues(List actors, Func getValue) + { + int sum = 0; + foreach (var a in actors) + if (a.HasTrait()) + sum += getValue(a); + return sum; + } + + protected float Average(List actors, Func getValue) + { + int sum = 0; + int countActors = 0; + foreach (var a in actors) + if (a.HasTrait()) + { + sum += getValue(a); + countActors++; + } + if (countActors == 0) return 0.0f; + return sum / countActors; + } + } +} diff --git a/OpenRA.Mods.RA/AI/BaseBuilder.cs b/OpenRA.Mods.RA/AI/BaseBuilder.cs index 7d656ac143..312870e9b0 100644 --- a/OpenRA.Mods.RA/AI/BaseBuilder.cs +++ b/OpenRA.Mods.RA/AI/BaseBuilder.cs @@ -74,7 +74,13 @@ namespace OpenRA.Mods.RA.AI lastThinkTick = ai.ticks; /* place the building */ - var location = ai.ChooseBuildLocation(currentBuilding.Item); + BuildingType type = BuildingType.Building; + if(Rules.Info[currentBuilding.Item].Traits.Contains()) + type = BuildingType.Defense; + else if(Rules.Info[currentBuilding.Item].Traits.Contains()) + type = BuildingType.Refinery; + + var location = ai.ChooseBuildLocation(currentBuilding.Item, type); if (location == null) { HackyAI.BotDebug("AI: Nowhere to place {0}".F(currentBuilding.Item)); diff --git a/OpenRA.Mods.RA/AI/HackyAI.cs b/OpenRA.Mods.RA/AI/HackyAI.cs index 248d69a8d8..12fbda1e62 100644 --- a/OpenRA.Mods.RA/AI/HackyAI.cs +++ b/OpenRA.Mods.RA/AI/HackyAI.cs @@ -14,28 +14,25 @@ using System.Linq; using OpenRA.FileFormats; using OpenRA.Mods.RA.Buildings; using OpenRA.Mods.RA.Move; +using OpenRA.Mods.RA.Effects; +using OpenRA.Mods.RA.Air; using OpenRA.Traits; using XRandom = OpenRA.Thirdparty.Random; -//TODO: -// effectively clear the area around the production buildings' spawn points. -// don't spam the build unit button, only queue one unit then wait for the backoff period. -// just make the build unit action only occur once every second. - -// later: -// don't build units randomly, have a method to it. -// explore spawn points methodically -// once you find a player, attack the player instead of spawn points. - namespace OpenRA.Mods.RA.AI { class HackyAIInfo : IBotInfo, ITraitInfo { public readonly string Name = "Unnamed Bot"; public readonly int SquadSize = 8; + + //intervals public readonly int AssignRolesInterval = 20; + public readonly int RushInterval = 600; + public readonly int AttackForceInterval = 30; + public readonly string RallypointTestBuilding = "fact"; // temporary hack to maintain previous rallypoint behavior. - public readonly string[] UnitQueues = {"Vehicle", "Infantry", "Plane"}; + public readonly string[] UnitQueues = { "Vehicle", "Infantry", "Plane", "Ship", "Aircraft" }; public readonly bool ShouldRepairBuildings = true; string IBotInfo.Name { get { return this.Name; } } @@ -46,16 +43,42 @@ namespace OpenRA.Mods.RA.AI [FieldLoader.LoadUsing("LoadBuildings")] public readonly Dictionary BuildingFractions = null; + [FieldLoader.LoadUsing("LoadUnitsGeneralNames")] + public readonly Dictionary UnitsGeneralNames = null; + + [FieldLoader.LoadUsing("LoadBuildingsGeneralNames")] + public readonly Dictionary BuildingGeneralNames = null; + + [FieldLoader.LoadUsing("LoadBuildingLimits")] + public readonly Dictionary BuildingLimits = null; + static object LoadActorList(MiniYaml y, string field) { - return y.NodesDict[field].Nodes.ToDictionary( - t => t.Key, - t => FieldLoader.GetValue(field, t.Value.Value)); + return LoadList(y, field); + } + + static object LoadListList(MiniYaml y, string field) + { + return LoadList(y,field); + } + + static object LoadList(MiniYaml y, string field) + { + return y.NodesDict.ContainsKey(field) + ? y.NodesDict[field].NodesDict.ToDictionary( + a => a.Key, + a => FieldLoader.GetValue(field, a.Value.Value)) + : new Dictionary(); } static object LoadUnits(MiniYaml y) { return LoadActorList(y, "UnitsToBuild"); } static object LoadBuildings(MiniYaml y) { return LoadActorList(y, "BuildingFractions"); } + static object LoadUnitsGeneralNames(MiniYaml y) { return LoadListList(y, "UnitsGeneralNames"); } + static object LoadBuildingsGeneralNames(MiniYaml y) { return LoadListList(y, "BuildingGeneralNames"); } + + static object LoadBuildingLimits(MiniYaml y) { return LoadList(y, "BuildingLimits"); } + public object Create(ActorInitializer init) { return new HackyAI(this); } } @@ -63,21 +86,576 @@ namespace OpenRA.Mods.RA.AI class Enemy { public int Aggro; } + enum SquadType { Assault, Air, Rush, Protection } + + enum BuildingType { Building, Defense, Refinery } + + class Squad + { + public List units = new List(); + public SquadType type; + + World world; + HackyAI bot; + XRandom random; + + Actor target; + StateMachine fsm; + + //fuzzy + AttackOrFleeFuzzy attackOrFleeFuzzy = new AttackOrFleeFuzzy(); + + public Squad(HackyAI bot, SquadType type) : this(bot, type, null) { } + + public Squad(HackyAI bot, SquadType type, Actor target) + { + this.bot = bot; + this.world = bot.world; + this.random = bot.random; + this.type = type; + this.target = target; + fsm = new StateMachine(this); + + switch (type) + { + case SquadType.Assault: + case SquadType.Rush: + fsm.ChangeState(new GroundUnitsIdleState(), true); + break; + case SquadType.Air: + fsm.ChangeState(new AirIdleState(), true); + break; + case SquadType.Protection: + fsm.ChangeState(new UnitsForProtectionIdleState(), true); + break; + } + } + + public void Update() + { + if (IsEmpty) return; + fsm.UpdateFsm(); + } + + public bool IsEmpty + { + get { return !units.Any(); } + } + + public Actor Target + { + get { return target; } + set { target = value; } + } + + public bool TargetIsValid + { + get { return (target != null && !target.IsDead() && !target.Destroyed && target.IsInWorld); } + } + + //********************************************************************************** + // Squad AI States + + /* Include general functional for all states */ + + abstract class StateBase + { + protected const int dangerRadius = 10; + + protected virtual bool MayBeFlee(Squad owner, Func, bool> flee) + { + if (owner.IsEmpty) return false; + var u = owner.units.Random(owner.random); + + var units = owner.world.FindUnitsInCircle(u.CenterLocation, Game.CellSize * dangerRadius).ToList(); + var ownBaseBuildingAround = units.Where(unit => unit.Owner == owner.bot.p && unit.HasTrait()).ToList(); + if (ownBaseBuildingAround.Count > 0) return false; + + var enemyAroundUnit = units.Where(unit => owner.bot.p.Stances[unit.Owner] == Stance.Enemy && unit.HasTrait()).ToList(); + if (!enemyAroundUnit.Any()) return false; + + return flee(enemyAroundUnit); + } + + protected CPos? AverageUnitsPosition(List units) + { + int x = 0; + int y = 0; + int countUnits = 0; + foreach (var u in units) + if (u.Location != null) + { + x += u.Location.X; + y += u.Location.Y; + countUnits++; + } + x = x / countUnits; + y = y / countUnits; + return (x != 0 && y != 0) ? new CPos?(new CPos(x, y)) : null; + } + + protected void GoToRandomOwnBuilding(Squad owner) + { + var loc = RandomBuildingLocation(owner); + foreach (var a in owner.units) + owner.world.IssueOrder(new Order("Move", a, false) { TargetLocation = loc }); + } + + protected CPos RandomBuildingLocation(Squad owner) + { + var location = owner.bot.baseCenter; + var buildings = owner.world.ActorsWithTrait() + .Where(a => a.Actor.Owner == owner.bot.p).Select(a => a.Actor).ToArray(); + if (buildings.Length > 0) + location = buildings.Random(owner.random).Location; + return location; + } + + protected bool BusyAttack(Actor a) + { + if (!a.IsIdle) + if (a.GetCurrentActivity().GetType() == typeof(OpenRA.Mods.RA.Activities.Attack) || + (a.GetCurrentActivity().NextActivity != null && + a.GetCurrentActivity().NextActivity.GetType() == typeof(OpenRA.Mods.RA.Activities.Attack))) + return true; + return false; + } + } + + /* States for air units */ + + abstract class AirStateBase : StateBase + { + protected const int missileUnitsMultiplier = 3; + + protected int CountAntiAirUnits(List units) + { + int missileUnitsCount = 0; + foreach (var unit in units) + if (unit != null && unit.HasTrait() && !unit.HasTrait() + && !unit.IsDisabled()) + foreach (var weap in unit.Trait().Weapons) + if (weap.Info.ValidTargets.Contains("Air")) + { + missileUnitsCount++; + break; + } + return missileUnitsCount; + } + + //checks the number of anti air enemies around units + protected virtual bool MayBeFlee(Squad owner) + { + return base.MayBeFlee(owner, (enemyAroundUnit) => + { + int missileUnitsCount = 0; + if (enemyAroundUnit.Count > 0) + missileUnitsCount = CountAntiAirUnits(enemyAroundUnit); + + if (missileUnitsCount * missileUnitsMultiplier > owner.units.Count) + return true; + + return false; + }); + } + + protected Actor FindDefenselessTarget(Squad owner) + { + Actor target = null; + FindSafePlace(owner, out target, true); + + return target == null ? null : target; + } + + protected CPos? FindSafePlace(Squad owner, out Actor detectedEnemyTarget, bool needTarget) + { + World world = owner.world; + detectedEnemyTarget = null; + int x = (world.Map.MapSize.X % dangerRadius) == 0 ? world.Map.MapSize.X : world.Map.MapSize.X + dangerRadius; + int y = (world.Map.MapSize.Y % dangerRadius) == 0 ? world.Map.MapSize.Y : world.Map.MapSize.Y + dangerRadius; + + for (int i = 0; i < x; i += dangerRadius * 2) + for (int j = 0; j < y; j += dangerRadius * 2) + { + CPos pos = new CPos(i, j); + if (NearToPosSafely(owner, pos.ToPPos(), out detectedEnemyTarget)) + { + if (needTarget) + { + if (detectedEnemyTarget == null) + continue; + else + return pos; + } + return pos; + } + } + return null; + } + + protected bool NearToPosSafely(Squad owner, PPos loc) + { + Actor a; + return NearToPosSafely(owner, loc, out a); + } + + protected bool NearToPosSafely(Squad owner, PPos loc, out Actor detectedEnemyTarget) + { + detectedEnemyTarget = null; + var unitsAroundPos = owner.world.FindUnitsInCircle(loc, Game.CellSize * dangerRadius) + .Where(unit => owner.bot.p.Stances[unit.Owner] == Stance.Enemy).ToList(); + + int missileUnitsCount = 0; + if (unitsAroundPos.Count > 0) + { + missileUnitsCount = CountAntiAirUnits(unitsAroundPos); + if (missileUnitsCount * missileUnitsMultiplier < owner.units.Count) + { + detectedEnemyTarget = unitsAroundPos.Random(owner.random); + return true; + } + else + return false; + } + return true; + } + + protected bool FullAmmo(Actor a) + { + var limitedAmmo = a.TraitOrDefault(); + return (limitedAmmo != null && limitedAmmo.FullAmmo()); + } + + protected bool HasAmmo(Actor a) + { + var limitedAmmo = a.TraitOrDefault(); + return (limitedAmmo != null && limitedAmmo.HasAmmo()); + } + + protected bool IsReloadable(Actor a) + { + return a.TraitOrDefault() != null; + } + } + + class AirIdleState : AirStateBase, IState + { + public void Enter(Squad owner) { } + + public void Execute(Squad owner) + { + if (owner.IsEmpty) return; + + if (MayBeFlee(owner)) + { + owner.fsm.ChangeState(new AirFleeState(), true); + return; + } + + var e = FindDefenselessTarget(owner); + if (e == null) + return; + else + { + owner.Target = e; + owner.fsm.ChangeState(new AirAttackState(), true); + } + } + + public void Exit(Squad owner) { } + } + + class AirAttackState : AirStateBase, IState + { + public void Enter(Squad owner) { } + + public void Execute(Squad owner) + { + if (owner.IsEmpty) return; + + if (!owner.TargetIsValid) + { + var a = owner.units.Random(owner.random); + var closestEnemy = owner.bot.FindClosestEnemy(a.CenterLocation); + if (closestEnemy != null) + owner.Target = closestEnemy; + else + { + owner.fsm.ChangeState(new AirFleeState(), true); + return; + } + } + + if (!NearToPosSafely(owner, owner.Target.CenterLocation)) + { + owner.fsm.ChangeState(new AirFleeState(), true); + return; + } + + foreach (var a in owner.units) + { + if (BusyAttack(a)) + continue; + if (!IsReloadable(a)) + if (!HasAmmo(a)) + continue; + if (owner.Target.HasTrait()) + owner.world.IssueOrder(new Order("Attack", a, false) { TargetActor = owner.Target }); + } + } + + public void Exit(Squad owner) { } + } + + class AirFleeState : AirStateBase, IState + { + public void Enter(Squad owner) { } + + public void Execute(Squad owner) + { + if (owner.IsEmpty) return; + + foreach (var a in owner.units) + { + if (!IsReloadable(a)) + if (!FullAmmo(a)) + { + owner.world.IssueOrder(new Order("ReturnToBase", a, false)); + continue; + } + owner.world.IssueOrder(new Order("Move", a, false) { TargetLocation = RandomBuildingLocation(owner) }); + } + owner.fsm.ChangeState(new AirIdleState(), true); + } + + public void Exit(Squad owner) { } + } + + /* States for ground units */ + + abstract class GroundStateBase : StateBase + { + protected virtual bool MayBeFlee(Squad owner) + { + return base.MayBeFlee(owner, (enemyAroundUnit) => + { + owner.attackOrFleeFuzzy.CalculateFuzzy(owner.units, enemyAroundUnit); + if (!owner.attackOrFleeFuzzy.CanAttack) + return true; + + return false; + }); + } + } + + class GroundUnitsIdleState : GroundStateBase, IState + { + public void Enter(Squad owner) { } + + public void Execute(Squad owner) + { + if (owner.IsEmpty) return; + if (!owner.TargetIsValid) + { + var t = owner.bot.FindClosestEnemy(owner.units.FirstOrDefault().CenterLocation); + if (t == null) return; + owner.Target = t; + } + + var enemyUnits = owner.world.FindUnitsInCircle(owner.Target.CenterLocation, Game.CellSize * 10) + .Where(unit => owner.bot.p.Stances[unit.Owner] == Stance.Enemy).ToList(); + if (enemyUnits.Any()) + { + owner.attackOrFleeFuzzy.CalculateFuzzy(owner.units, enemyUnits); + if (owner.attackOrFleeFuzzy.CanAttack) + { + foreach(var u in owner.units) + owner.world.IssueOrder(new Order("AttackMove", u, false) { TargetLocation = owner.Target.CenterLocation.ToCPos() }); + // We have gathered sufficient units. Attack the nearest enemy unit. + owner.fsm.ChangeState(new GroundUnitsAttackMoveState(), true); + return; + } + else + owner.fsm.ChangeState(new GroundUnitsFleeState(), true); + } + } + + public void Exit(Squad owner) { } + } + + class GroundUnitsAttackMoveState : GroundStateBase, IState + { + public void Enter(Squad owner) { } + + public void Execute(Squad owner) + { + if (owner.IsEmpty) return; + + if (!owner.TargetIsValid) + { + var closestEnemy = owner.bot.FindClosestEnemy(owner.units.Random(owner.random).CenterLocation); + if (closestEnemy != null) + owner.Target = closestEnemy; + else + { + owner.fsm.ChangeState(new GroundUnitsFleeState(), true); + return; + } + } + + Actor leader = owner.units.ClosestTo(owner.Target.CenterLocation); + if (leader == null) + return; + var ownUnits = owner.world.FindUnitsInCircle(leader.CenterLocation, Game.CellSize * 8) + .Where(a => a.Owner == owner.units.FirstOrDefault().Owner && owner.units.Contains(a)).ToList(); + if (ownUnits.Count < owner.units.Count) + { + owner.world.IssueOrder(new Order("Stop", leader, false)); + foreach (var unit in owner.units.Where(a => !ownUnits.Contains(a))) + owner.world.IssueOrder(new Order("AttackMove", unit, false) { TargetLocation = leader.CenterLocation.ToCPos() }); + } + else + { + var enemys = owner.world.FindUnitsInCircle(leader.CenterLocation, Game.CellSize * 12) + .Where(a1 => !a1.Destroyed && !a1.IsDead()).ToList(); + var enemynearby = enemys.Where(a1 => a1.HasTrait() && leader.Owner.Stances[a1.Owner] == Stance.Enemy).ToList(); + if (enemynearby.Any()) + { + owner.Target = enemynearby.ClosestTo(leader.CenterLocation); + owner.fsm.ChangeState(new GroundUnitsAttackState(), true); + return; + } + else + foreach (var a in owner.units) + owner.world.IssueOrder(new Order("AttackMove", a, false) { TargetLocation = owner.Target.Location }); + } + + if (MayBeFlee(owner)) + { + owner.fsm.ChangeState(new GroundUnitsFleeState(), true); + return; + } + } + + public void Exit(Squad owner) { } + } + + class GroundUnitsAttackState : GroundStateBase, IState + { + public void Enter(Squad owner) { } + + public void Execute(Squad owner) + { + if (owner.IsEmpty) return; + + if (!owner.TargetIsValid) + { + var closestEnemy = owner.bot.FindClosestEnemy(owner.units.Random(owner.random).CenterLocation); + if (closestEnemy != null) + owner.Target = closestEnemy; + else + { + owner.fsm.ChangeState(new GroundUnitsFleeState(), true); + return; + } + } + foreach (var a in owner.units) + if (!BusyAttack(a)) + owner.world.IssueOrder(new Order("Attack", a, false) { TargetActor = owner.bot.FindClosestEnemy(a.CenterLocation) }); + + if (MayBeFlee(owner)) + { + owner.fsm.ChangeState(new GroundUnitsFleeState(), true); + return; + } + } + + public void Exit(Squad owner) { } + } + + class GroundUnitsFleeState : GroundStateBase, IState + { + public void Enter(Squad owner) { } + + public void Execute(Squad owner) + { + if (owner.IsEmpty) return; + + GoToRandomOwnBuilding(owner); + owner.fsm.ChangeState(new GroundUnitsIdleState(), true); + } + + public void Exit(Squad owner) { owner.units.Clear(); } + } + + class UnitsForProtectionIdleState : GroundStateBase, IState + { + public void Enter(Squad owner) { } + public void Execute(Squad owner) { owner.fsm.ChangeState(new UnitsForProtectionAttackState(), true); } + public void Exit(Squad owner) { } + } + + class UnitsForProtectionAttackState : GroundStateBase, IState + { + public void Enter(Squad owner) { } + + public void Execute(Squad owner) + { + if (owner.IsEmpty) return; + if (!owner.TargetIsValid) + { + owner.Target = owner.bot.FindClosestEnemy(AverageUnitsPosition(owner.units).Value.ToPPos(), 8); + + if (owner.Target == null) + { + owner.fsm.ChangeState(new UnitsForProtectionFleeState(), true); + return; + } + } + foreach (var a in owner.units) + owner.world.IssueOrder(new Order("Attack", a, false) { TargetActor = owner.Target }); + } + + public void Exit(Squad owner) { } + } + + class UnitsForProtectionFleeState : GroundStateBase, IState + { + public void Enter(Squad owner) { } + + public void Execute(Squad owner) + { + if (owner.IsEmpty) return; + + GoToRandomOwnBuilding(owner); + owner.fsm.ChangeState(new UnitsForProtectionIdleState(), true); + } + + public void Exit(Squad owner) { owner.units.Clear(); } + } + } + class HackyAI : ITick, IBot, INotifyDamage { bool enabled; public int ticks; public Player p; + public XRandom random; + public CPos baseCenter; PowerManager playerPower; + SupportPowerManager supportPowerMngr; + PlayerResources playerResource; readonly BuildingInfo rallypointTestBuilding; // temporary hack - readonly HackyAIInfo Info; + internal readonly HackyAIInfo Info; + + string[] resourceTypes; + + RushFuzzy rushFuzzy = new RushFuzzy(); Cache aggro = new Cache( _ => new Enemy() ); - CPos baseCenter; - XRandom random = new XRandom(); //we do not use the synced random number generator. BaseBuilder[] builders; - const int MaxBaseDistance = 20; + const int MaxBaseDistance = 40; public const int feedbackTime = 30; // ticks; = a bit over 1s. must be >= netlag. public World world { get { return p.PlayerActor.World; } } @@ -102,9 +680,16 @@ namespace OpenRA.Mods.RA.AI this.p = p; enabled = true; playerPower = p.PlayerActor.Trait(); + supportPowerMngr = p.PlayerActor.Trait(); + playerResource = p.PlayerActor.Trait(); builders = new BaseBuilder[] { - new BaseBuilder( this, "Building", q => ChooseBuildingToBuild(q, true) ), - new BaseBuilder( this, "Defense", q => ChooseBuildingToBuild(q, false) ) }; + new BaseBuilder( this, "Building", q => ChooseBuildingToBuild(q, false) ), + new BaseBuilder( this, "Defense", q => ChooseBuildingToBuild(q, true) ) }; + + random = new XRandom((int)p.PlayerActor.ActorID); + + resourceTypes = Rules.Info["world"].Traits.WithInterface() + .Select(t => t.TerrainType).ToArray(); } int GetPowerProvidedBy(ActorInfo building) @@ -121,6 +706,56 @@ namespace OpenRA.Mods.RA.AI return buildableThings.ElementAtOrDefault(random.Next(buildableThings.Count())); } + ActorInfo ChooseUnitToBuild(ProductionQueue queue) + { + var buildableThings = queue.BuildableItems(); + if (!buildableThings.Any()) return null; + + var myUnits = p.World + .ActorsWithTrait() + .Where(a => a.Actor.Owner == p) + .Select(a => a.Actor.Info.Name).ToArray(); + + foreach (var unit in Info.UnitsToBuild) + if (buildableThings.Any(b => b.Name == unit.Key)) + if (myUnits.Count(a => a == unit.Key) < unit.Value * myUnits.Length) + return Rules.Info[unit.Key]; + + return null; + } + + int CountBuilding(string frac, Player owner) + { + return world.ActorsWithTrait().Where(a => a.Actor.Owner == owner && a.Actor.Info.Name == frac).Count(); + } + + int? CountBuildingByGeneralName(string generalName, Player owner) + { + if(Info.BuildingGeneralNames.ContainsKey(generalName)) + return world.ActorsWithTrait() + .Where(a => a.Actor.Owner == owner && Info.BuildingGeneralNames[generalName].Contains(a.Actor.Info.Name)).Count(); + return null; + } + + ActorInfo GetBuildingInfoByGeneralName(string generalName, Player owner) + { + if (generalName == "ConstructionYard") + return Rules.Info.Where(k => Info.BuildingGeneralNames[generalName].Contains(k.Key)).Random(random).Value; + return GetInfoByGeneralName(Info.BuildingGeneralNames, generalName, owner); + } + + ActorInfo GetUnitInfoByGeneralName(string generalName, Player owner) + { + return GetInfoByGeneralName(Info.UnitsGeneralNames, generalName, owner); + } + + ActorInfo GetInfoByGeneralName(Dictionary names, string generalName, Player owner) + { + if (names[generalName] == null) return null; + return Rules.Info.Where(k => names[generalName].Contains(k.Key) && + k.Value.Traits.Get().Owner.Contains(owner.Country.Race)).Random(random).Value; //random is shit*/ + } + bool HasAdequatePower() { /* note: CNC `fact` provides a small amount of power. don't get jammed because of that. */ @@ -128,19 +763,56 @@ namespace OpenRA.Mods.RA.AI playerPower.PowerProvided > playerPower.PowerDrained * 1.2; } - ActorInfo ChooseBuildingToBuild(ProductionQueue queue, bool buildPower) + public bool HasAdequateFact() + { + if (CountBuildingByGeneralName("ConstructionYard", p) == 0 && CountBuildingByGeneralName("VehiclesFactory", p) > 0) + return false; + return true; + } + + public bool HasAdequateProc() + { + if (CountBuildingByGeneralName("Refinery", p) == 0 && CountBuildingByGeneralName("Power", p) > 0) + return false; + return true; + } + + public bool HasMinimumProc() + { + if (CountBuildingByGeneralName("Refinery", p) < 2 && CountBuildingByGeneralName("Power", p) > 0 && + CountBuildingByGeneralName("Barracks",p) > 0) + return false; + return true; + } + + public bool HasAdequateNumber(string frac, Player owner) + { + if (Info.BuildingLimits.ContainsKey(frac)) + if (CountBuilding(frac, owner) < Info.BuildingLimits[frac]) + return true; + else + return false; + + return true; + } + + ActorInfo ChooseBuildingToBuild(ProductionQueue queue, bool isDefense) { var buildableThings = queue.BuildableItems(); - if (!HasAdequatePower()) /* try to maintain 20% excess power */ + if (!isDefense) { - if (!buildPower) return null; + if (!HasAdequatePower()) /* try to maintain 20% excess power */ + /* find the best thing we can build which produces power */ + return buildableThings.Where(a => GetPowerProvidedBy(a) > 0) + .OrderByDescending(a => GetPowerProvidedBy(a)).FirstOrDefault(); - /* find the best thing we can build which produces power */ - return buildableThings.Where(a => GetPowerProvidedBy(a) > 0) - .OrderByDescending(a => GetPowerProvidedBy(a)).FirstOrDefault(); + if (playerResource.AlertSilo) + return GetBuildingInfoByGeneralName("Silo", p); + + if (!HasAdequateProc() || !HasMinimumProc()) + return GetBuildingInfoByGeneralName("Refinery", p); } - var myBuildings = p.World .ActorsWithTrait() .Where( a => a.Actor.Owner == p ) @@ -148,7 +820,7 @@ namespace OpenRA.Mods.RA.AI 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 && + if (myBuildings.Count(a => a == frac.Key) < frac.Value * myBuildings.Length && HasAdequateNumber(frac.Key, p) && playerPower.ExcessPower >= Rules.Info[frac.Key].Traits.Get().Power) return Rules.Info[frac.Key]; @@ -161,16 +833,58 @@ namespace OpenRA.Mods.RA.AI return cells.All(c => bi.GetBuildingAt(c) == null); } - public CPos? ChooseBuildLocation(string actorType) + 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) { var bi = Rules.Info[actorType].Traits.Get(); + if (bi == null) return null; - foreach (var t in world.FindTilesInCircle(baseCenter, MaxBaseDistance)) - if (world.CanPlaceBuilding(actorType, bi, t, null)) - if (bi.IsCloseEnoughToBase(world, p, actorType, t)) - if (NoBuildingsUnder(Util.ExpandFootprint( - FootprintUtils.Tiles(actorType, bi, t), false))) - return t; + Func findPos = (PPos pos, CPos center) => + { + for (var k = MaxBaseDistance; k >= 0; k--) + if (pos != null) + { + var tlist = world.FindTilesInCircle(center, k) + .OrderBy(a => (new PPos(a.ToPPos().X, a.ToPPos().Y) - pos).LengthSquared); + 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(actorType, bi, t), false))) + return t; + } + return null; + }; + + switch(type) + { + case BuildingType.Defense: + Actor enemyBase = FindEnemyBuildingClosestToPos(baseCenter.ToPPos()); + return enemyBase != null ? findPos(enemyBase.CenterLocation, defenseCenter) : null; + + case BuildingType.Refinery: + var pos = world.FindTilesInCircle(baseCenter, MaxBaseDistance) + .Where(a => resourceTypes.Contains(world.GetTerrainType(new CPos(a.X, a.Y)))) + .OrderBy(a => (new PPos(a.ToPPos().X, a.ToPPos().Y) - baseCenter.ToPPos()).LengthSquared).First(); + return findPos(pos.ToPPos(), baseCenter); + + case BuildingType.Building: + for (var k = 0; k < maxBaseDistance; k++) + foreach (var t in world.FindTilesInCircle(baseCenter, k)) + if (world.CanPlaceBuilding(actorType, bi, t, null)) + { + if (distanceToBaseIsImportant) + if (!bi.IsCloseEnoughToBase(world, p, actorType, t)) + return null; + if (NoBuildingsUnder(Util.ExpandFootprint(FootprintUtils.Tiles(actorType, bi, t), false))) + return t; + } + break; + } return null; // i don't know where to put it. } @@ -186,48 +900,43 @@ namespace OpenRA.Mods.RA.AI DeployMcv(self); if (ticks % feedbackTime == 0) - foreach (var q in Info.UnitQueues) - BuildRandom(q); - + ProductionUnits(self); + AssignRolesToIdleUnits(self); SetRallyPointsForNewProductionBuildings(self); + TryToUseSupportPower(self); foreach (var b in builders) b.Tick(); } - //hacks etc sigh mess. - //A bunch of hardcoded lists to keep track of which units are doing what. - List unitsHangingAroundTheBase = new List(); - List attackForce = new List(); - CPos? attackTarget; - - //Units that the ai already knows about. Any unit not on this list needs to be given a role. - List activeUnits = new List(); - - CPos? ChooseEnemyTarget() + internal Actor ChooseEnemyTarget() { var liveEnemies = world.Players .Where(q => p != q && p.Stances[q] == Stance.Enemy) .Where(q => p.WinState == WinState.Undefined && q.WinState == WinState.Undefined); + if (!liveEnemies.Any()) + return null; + var leastLikedEnemies = liveEnemies .GroupBy(e => aggro[e].Aggro) .OrderByDescending(g => g.Key) .FirstOrDefault(); + Player enemy; if (leastLikedEnemies == null) - return null; - - var enemy = leastLikedEnemies.Random(random); + enemy = liveEnemies.FirstOrDefault(); + else + enemy = leastLikedEnemies.Random(random); /* pick something worth attacking owned by that player */ var targets = world.Actors - .Where(a => a.Owner == enemy && a.HasTrait() && !a.HasTrait()); + .Where(a => a.Owner == enemy && a.HasTrait()); Actor target = null; - if (targets.Any()) - target = targets.Random(random); + if (targets.Any() && baseCenter != null) + target = targets.ClosestTo(baseCenter.ToPPos()); if (target == null) { @@ -242,126 +951,213 @@ namespace OpenRA.Mods.RA.AI if (leastLikedEnemies.Count() > 1) aggro[enemy].Aggro++; - return target.Location; + return target; + } + + internal Actor FindClosestEnemy(PPos pos) + { + var allEnemyUnits = world.Actors + .Where(unit => p.Stances[unit.Owner] == Stance.Enemy && !unit.HasTrait() && + unit.HasTrait() && unit.HasTrait()).ToList(); + + if (allEnemyUnits.Count > 0) + return allEnemyUnits.ClosestTo(pos); + return null; + } + + internal Actor FindClosestEnemy(PPos pos, int radius) + { + var enemyUnits = world.FindUnitsInCircle(pos, Game.CellSize * radius) + .Where(unit => p.Stances[unit.Owner] == Stance.Enemy && + !unit.HasTrait() && unit.HasTrait()).ToList(); + + if (enemyUnits.Count > 0) + return enemyUnits.ClosestTo(pos); + return null; + } + + List FindEnemyConstructionYards() + { + var bases = world.Actors.Where(a => p.Stances[a.Owner] == Stance.Enemy && a.HasTrait() + && !a.Destroyed && a.HasTrait() && !a.HasTrait()).ToList(); + return bases != null ? bases : new List(); + } + + Actor FindEnemyBuildingClosestToPos(PPos pos) + { + var closestBuilding = world.Actors.Where(a => p.Stances[a.Owner] == Stance.Enemy && a.HasTrait() + && !a.Destroyed && a.HasTrait()).ClosestTo(pos); + return closestBuilding; + } + + List squads = new List(); + + List unitsHangingAroundTheBase = new List(); + //Units that the ai already knows about. Any unit not on this list needs to be given a role. + List activeUnits = new List(); + + void CleanSquads() + { + squads.RemoveAll(s => s.IsEmpty); + foreach (Squad squad in squads) + squad.units.RemoveAll(a => a.Destroyed || a.IsDead()); + } + + //use of this function requires that one squad of this type. Hence it is a piece of shit + Squad GetSquadOfType(SquadType type) + { + return squads.Where(s => s.type == type).FirstOrDefault(); + } + + Squad RegisterNewSquad(SquadType type, Actor target = null) + { + var ret = new Squad(this, type, target); + squads.Add(ret); + return ret; } int assignRolesTicks = 0; + int rushTicks = 0; + int attackForceTicks = 0; void AssignRolesToIdleUnits(Actor self) { - //HACK: trim these lists -- we really shouldn't be hanging onto all this state - //when it's invalidated so easily, but that's Matthew/Alli's problem. - activeUnits.RemoveAll(a => a.Destroyed); - unitsHangingAroundTheBase.RemoveAll(a => a.Destroyed); - attackForce.RemoveAll(a => a.Destroyed); + CleanSquads(); + activeUnits.RemoveAll(a => a.Destroyed || a.IsDead()); + unitsHangingAroundTheBase.RemoveAll(a => a.Destroyed || a.IsDead()); + + if (--rushTicks <= 0) + { + rushTicks = Info.RushInterval; + TryToRushAttack(); + } + + if (--attackForceTicks <= 0) + { + attackForceTicks = Info.AttackForceInterval; + foreach (var s in squads) + s.Update(); + } if (--assignRolesTicks > 0) return; else assignRolesTicks = Info.AssignRolesInterval; + GiveOrdersToIdleHarvesters(); + FindNewUnits(self); + CreateAttackForce(); + FindAndDeployMcv(self); + } + + void GiveOrdersToIdleHarvesters() + { // Find idle harvesters and give them orders: foreach (var a in activeUnits) { var harv = a.TraitOrDefault(); if (harv == null) continue; + if (!a.IsIdle) { Activity act = a.GetCurrentActivity(); // A Wait activity is technically idle: - if (!(act is Activities.Wait) && - (act.NextActivity == null || !(act.NextActivity is Activities.FindResources))) + if ((act.GetType() != typeof(OpenRA.Mods.RA.Activities.Wait)) && + (act.NextActivity == null || act.NextActivity.GetType() != typeof(OpenRA.Mods.RA.Activities.FindResources))) continue; } if (!harv.IsEmpty) continue; // Tell the idle harvester to quit slacking: world.IssueOrder(new Order("Harvest", a, false)); - } + } + } + void FindNewUnits(Actor self) + { var newUnits = self.World.ActorsWithTrait() .Where(a => a.Actor.Owner == p && !a.Actor.HasTrait() - && !activeUnits.Contains(a.Actor) && a.Actor.IsInWorld) - .Select(a => a.Actor).ToArray(); + && !activeUnits.Contains(a.Actor)) + .Select(a => a.Actor).ToArray(); foreach (var a in newUnits) { BotDebug("AI: Found a newly built unit"); if (a.HasTrait()) - world.IssueOrder( new Order( "Harvest", a, false ) ); + world.IssueOrder(new Order("Harvest", a, false)); else unitsHangingAroundTheBase.Add(a); + if (a.HasTrait() && a.HasTrait()) + { + var air = GetSquadOfType(SquadType.Air); + if (air == null) + air = RegisterNewSquad(SquadType.Air); + air.units.Add(a); + } activeUnits.Add(a); - } + } + } + void CreateAttackForce() + { /* Create an attack force when we have enough units around our base. */ // (don't bother leaving any behind for defense.) - - int randomizedSquadSize = Info.SquadSize - 4 + random.Next(200); + var randomizedSquadSize = Info.SquadSize + random.Next(30); if (unitsHangingAroundTheBase.Count >= randomizedSquadSize) { - BotDebug("Launch an attack."); + var attackForce = RegisterNewSquad(SquadType.Assault); - if (attackForce.Count == 0) + foreach (var a in unitsHangingAroundTheBase) + if (!a.HasTrait()) + attackForce.units.Add(a); + unitsHangingAroundTheBase.Clear(); + } + } + + void TryToRushAttack() + { + var allEnemyBaseBuilder = FindEnemyConstructionYards(); + var ownUnits = activeUnits + .Where(unit => unit.HasTrait() && !unit.HasTrait() && unit.IsIdle).ToList(); + if (!allEnemyBaseBuilder.Any() || (ownUnits.Count < Info.SquadSize)) return; + foreach (var b in allEnemyBaseBuilder) + { + var enemys = world.FindUnitsInCircle(b.CenterLocation, Game.CellSize * 15) + .Where(unit => p.Stances[unit.Owner] == Stance.Enemy && unit.HasTrait()).ToList(); + + rushFuzzy.CalculateFuzzy(ownUnits, enemys); + if (rushFuzzy.CanAttack) { - attackTarget = ChooseEnemyTarget(); - if (attackTarget == null) - return; + var target = enemys.Any() ? enemys.Random(random) : b; + var rush = GetSquadOfType(SquadType.Rush); + if (rush == null) + rush = RegisterNewSquad(SquadType.Rush, target); - foreach (var a in unitsHangingAroundTheBase) - if (TryToMove(a, attackTarget.Value, true)) - attackForce.Add(a); - - unitsHangingAroundTheBase.Clear(); + foreach (var a3 in ownUnits) + rush.units.Add(a3); + + return; } } + } - // If we have any attackers, let them scan for enemy units and stop and regroup if they spot any - if (attackForce.Count > 0) + void ProtectOwn(Actor attacker) + { + var protectSq = GetSquadOfType(SquadType.Protection); + if (protectSq == null) + protectSq = RegisterNewSquad(SquadType.Protection, attacker); + + if (!protectSq.TargetIsValid) + protectSq.Target = attacker; + if (protectSq.IsEmpty) { - bool foundEnemy = false; - foreach (var a1 in attackForce) - { - var enemyUnits = world.FindUnitsInCircle(a1.CenterLocation, Game.CellSize * 10) - .Where(unit => p.Stances[unit.Owner] == Stance.Enemy && !unit.HasTrait()).ToList(); - - if (enemyUnits.Count > 0) - { - // Found enemy units nearby. - foundEnemy = true; - var enemy = enemyUnits.ClosestTo( a1.CenterLocation ); - - // Check how many own units we have gathered nearby... - var ownUnits = world.FindUnitsInCircle(a1.CenterLocation, Game.CellSize * 2) - .Where(unit => unit.Owner == p).ToList(); - if (ownUnits.Count < randomizedSquadSize) - { - // Not enough to attack. Send more units. - world.IssueOrder(new Order("Stop", a1, false)); - - foreach (var a2 in attackForce) - if (a2 != a1) - world.IssueOrder(new Order("AttackMove", a2, false) { TargetLocation = a1.Location }); - } - else - { - // We have gathered sufficient units. Attack the nearest enemy unit. - foreach (var a2 in attackForce) - world.IssueOrder(new Order("Attack", a2, false) { TargetActor = enemy }); - } - return; - } - } - - if (!foundEnemy) - { - attackTarget = ChooseEnemyTarget(); - if (attackTarget != null) - foreach (var a in attackForce) - TryToMove(a, attackTarget.Value, true); - } + var ownUnits = world.FindUnitsInCircle(baseCenter.ToPPos(), Game.CellSize * 15) + .Where(unit => unit.Owner == p && !unit.HasTrait() + && unit.HasTrait()).ToList(); + foreach (var a in ownUnits) + protectSq.units.Add(a); } } @@ -402,37 +1198,6 @@ namespace OpenRA.Mods.RA.AI return possibleRallyPoints.Random(random); } - CPos? ChooseDestinationNear(Actor a, CPos desiredMoveTarget) - { - var move = a.TraitOrDefault(); - if (move == null) return null; - - CPos xy; - int loopCount = 0; //avoid infinite loops. - int range = 2; - do - { - //loop until we find a valid move location - xy = new CPos(desiredMoveTarget.X + random.Next(-range, range), desiredMoveTarget.Y + random.Next(-range, range)); - loopCount++; - range = Math.Max(range, loopCount / 2); - if (loopCount > 10) return null; - } while (!move.CanEnterCell(xy) && xy != a.Location); - - return xy; - } - - //try very hard to find a valid move destination near the target. - //(Don't accept a move onto the subject's current position. maybe this is already not allowed? ) - bool TryToMove(Actor a, CPos desiredMoveTarget, bool attackMove) - { - var xy = ChooseDestinationNear(a, desiredMoveTarget); - if (xy == null) - return false; - world.IssueOrder(new Order(attackMove ? "AttackMove" : "Move", a, false) { TargetLocation = xy.Value }); - return true; - } - void DeployMcv(Actor self) { /* find our mcv and deploy it */ @@ -442,16 +1207,83 @@ namespace OpenRA.Mods.RA.AI if (mcv != null) { baseCenter = mcv.Location; + defenseCenter = baseCenter; //Don't transform the mcv if it is a fact if (mcv.HasTrait()) - { world.IssueOrder(new Order("DeployTransform", mcv, false)); - } } else BotDebug("AI: Can't find BaseBuildUnit."); } + void FindAndDeployMcv(Actor self) + { + var mcvs = self.World.Actors.Where(a => a.Owner == p && a.HasTrait()).ToArray(); + if (!mcvs.Any()) + return; + else + foreach (var mcv in mcvs) + if (mcv != null) + //Don't transform the mcv if it is a fact + if (mcv.HasTrait()) + { + if (mcv.IsMoving()) return; + var maxBaseDistance = world.Map.MapSize.X > world.Map.MapSize.Y ? world.Map.MapSize.X : world.Map.MapSize.Y; + ActorInfo aInfo = GetUnitInfoByGeneralName("Mcv",p); + if (aInfo == null) return; + string intoActor = aInfo.Traits.Get().IntoActor; + var desiredLocation = ChooseBuildLocation(intoActor, false, maxBaseDistance, BuildingType.Building); + if (desiredLocation == null) + return; + world.IssueOrder(new Order("Move", mcv, false) { TargetLocation = desiredLocation.Value }); + world.IssueOrder(new Order("DeployTransform", mcv, false)); + } + } + + void TryToUseSupportPower(Actor self) + { + if (supportPowerMngr == null) return; + var powers = supportPowerMngr.Powers.Where(p => !p.Value.Disabled); + + foreach (var kv in powers) + { + var sp = kv.Value; + if (sp.Ready) + { + var attackLocation = FindAttackLocationToSupportPower(5); + if (attackLocation == null) return; + sp.Activate(new Order() { TargetLocation = attackLocation.Value }); + } + } + } + + CPos? FindAttackLocationToSupportPower(int radiusOfPower) + { + CPos? resLoc = null; + int countUnits = 0; + + int x = (world.Map.MapSize.X % radiusOfPower) == 0 ? world.Map.MapSize.X : world.Map.MapSize.X + radiusOfPower; + int y = (world.Map.MapSize.Y % radiusOfPower) == 0 ? world.Map.MapSize.Y : world.Map.MapSize.Y + radiusOfPower; + + for (int i = 0; i < x; i += radiusOfPower * 2) + for (int j = 0; j < y; j += radiusOfPower * 2) + { + CPos pos = new CPos(i, j); + var targets = world.FindUnitsInCircle(pos.ToPPos(), Game.CellSize * radiusOfPower).ToList(); + var enemys = targets.Where(unit => p.Stances[unit.Owner] == Stance.Enemy).ToList(); + var ally = targets.Where(unit => p.Stances[unit.Owner] == Stance.Ally || unit.Owner == p).ToList(); + + if (enemys.Count < ally.Count || !enemys.Any()) + continue; + if (enemys.Count > countUnits) + { + countUnits = enemys.Count; + resLoc = enemys.Random(random).Location; + } + } + return resLoc; + } + internal IEnumerable FindQueues(string category) { return world.ActorsWithTrait() @@ -459,22 +1291,51 @@ namespace OpenRA.Mods.RA.AI .Select(a => a.Trait); } - //Build a random unit of the given type. Not going to be needed once there is actual AI... - void BuildRandom(string category) + void ProductionUnits(Actor self) + { + if (!HasAdequateProc()) /* Stop building until economy is back on */ + return; + if (!HasAdequateFact()) + if (!self.World.Actors.Where(a => a.Owner == p && a.HasTrait() && a.HasTrait()).Any()) + BuildUnit("Vehicle", GetUnitInfoByGeneralName("Mcv",p).Name); + foreach (var q in Info.UnitQueues) + { + if (unitsHangingAroundTheBase.Count < 12) + BuildUnit(q, true); + BuildUnit(q, false); + } + } + + void BuildUnit(string category, bool buildRandom) { // Pick a free queue - var queue = FindQueues( category ).FirstOrDefault( q => q.CurrentItem() == null ); + var queue = FindQueues(category).FirstOrDefault( q => q.CurrentItem() == null ); if (queue == null) return; - var unit = ChooseRandomUnitToBuild(queue); - if (unit != null && Info.UnitsToBuild.Any( u => u.Key == unit.Name )) + ActorInfo unit; + if(buildRandom) + unit = ChooseRandomUnitToBuild(queue); + else + unit = ChooseUnitToBuild(queue); + + if (unit != null && Info.UnitsToBuild.Any(u => u.Key == unit.Name)) world.IssueOrder(Order.StartProduction(queue.self, unit.Name, 1)); } + void BuildUnit(string category, string name) + { + var queue = FindQueues(category).FirstOrDefault( q => q.CurrentItem() == null ); + if (queue == null) return; + if(Rules.Info[name] != null) + world.IssueOrder(Order.StartProduction(queue.self, name, 1)); + } + public void Damaged(Actor self, AttackInfo e) { if (!enabled) return; + if (e.Attacker.Destroyed) return; + if (!e.Attacker.HasTrait()) return; if (Info.ShouldRepairBuildings && self.HasTrait()) if (e.DamageState > DamageState.Light && e.PreviousDamageState <= DamageState.Light) @@ -487,6 +1348,14 @@ namespace OpenRA.Mods.RA.AI if (e.Attacker != null && e.Damage > 0) aggro[e.Attacker.Owner].Aggro += e.Damage; + + //protected harvesters or building + if (self.HasTrait() || self.HasTrait()) + if (e.Attacker.HasTrait() && (p.Stances[e.Attacker.Owner] == Stance.Enemy)) + { + defenseCenter = e.Attacker.Location; + ProtectOwn(e.Attacker); + } } } } diff --git a/OpenRA.Mods.RA/AI/RushFuzzy.cs b/OpenRA.Mods.RA/AI/RushFuzzy.cs new file mode 100644 index 0000000000..d717db604a --- /dev/null +++ b/OpenRA.Mods.RA/AI/RushFuzzy.cs @@ -0,0 +1,39 @@ +#region Copyright & License Information +/* + * Copyright 2007-2011 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using AI.Fuzzy.Library; + +namespace OpenRA.Mods.RA.AI +{ + class RushFuzzy : AttackOrFleeFuzzy + { + public RushFuzzy() : base() { } + + protected override void AddingRulesForNormalOwnHealth() + { + //1 OwnHealth is Normal + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if ((OwnHealth is Normal) " + + "and ((EnemyHealth is NearDead) or (EnemyHealth is Injured) or (EnemyHealth is Normal)) " + + "and (RelativeAttackPower is Strong) " + + "and ((RelativeSpeed is Slow) or (RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " + + "then AttackOrFlee is Attack")); + + fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule("if ((OwnHealth is Normal) " + + "and ((EnemyHealth is NearDead) or (EnemyHealth is Injured) or (EnemyHealth is Normal)) " + + "and ((RelativeAttackPower is Weak) or (RelativeAttackPower is Equal)) " + + "and ((RelativeSpeed is Slow) or (RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " + + "then AttackOrFlee is Flee")); + } + } +} diff --git a/OpenRA.Mods.RA/AI/StateMachine.cs b/OpenRA.Mods.RA/AI/StateMachine.cs new file mode 100644 index 0000000000..f3ac9007f1 --- /dev/null +++ b/OpenRA.Mods.RA/AI/StateMachine.cs @@ -0,0 +1,79 @@ +#region Copyright & License Information +/* + * Copyright 2007-2011 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. For more information, + * see COPYING. + */ +#endregion + +namespace OpenRA.Mods.RA.AI +{ + class StateMachine + { + //a pointer to the agent that owns this instance + private Squad owner; + + private IState currentState; + + //a record of the last state the agent was in + private IState previousState; + + public StateMachine(Squad owner) + { + this.owner = owner; + } + + public IState CurrentState + { + get { return currentState; } + set { currentState = value; } + } + + public IState PreviousState + { + get { return previousState; } + set { previousState = value; } + } + + //call this to update the FSM + public void UpdateFsm() + { + currentState.Execute(owner); + } + + //change to a new state + //boolean variable isSaveCurrentState respons on save or not current state + public void ChangeState(IState newState, bool saveCurrentState) + { + if (saveCurrentState) + //keep a record of the previous state + previousState = currentState; + + //call the exit method of the existing state + if(currentState != null) + currentState.Exit(owner); + + //change state to the new state + if (newState != null) + currentState = newState; + + //call the entry method of the new state + currentState.Enter(owner); + } + + //change state back to the previous state + public void RevertToPreviousState(bool saveCurrentState) + { + ChangeState(previousState, saveCurrentState); + } + } + + interface IState + { + void Enter(Squad bot); + void Execute(Squad bot); + void Exit(Squad bot); + } +} diff --git a/OpenRA.Mods.RA/Move/Mobile.cs b/OpenRA.Mods.RA/Move/Mobile.cs index 35b198f81c..da7f4e0b5e 100755 --- a/OpenRA.Mods.RA/Move/Mobile.cs +++ b/OpenRA.Mods.RA/Move/Mobile.cs @@ -229,7 +229,22 @@ namespace OpenRA.Mods.RA.Move public CPos NearestMoveableCell(CPos target, int minRange, int maxRange) { - return NearestCell(target, CanEnterCell, minRange, maxRange); + if (CanEnterCell(target)) + return target; + + var searched = new List(); + // Limit search to a radius of 10 tiles + for (int r = minRange; r < maxRange; r++) + foreach (var tile in self.World.FindTilesInCircle(target, r).Except(searched)) + { + if (CanEnterCell(tile)) + return tile; + + searched.Add(tile); + } + + // Couldn't find a cell + return target; } public CPos NearestCell(CPos target, Func check, int minRange, int maxRange) @@ -237,9 +252,15 @@ namespace OpenRA.Mods.RA.Move if (check(target)) return target; - foreach (var tile in self.World.FindTilesInCircle(target, maxRange)) - if (check(tile)) - return tile; + var searched = new List(); + for (int r = minRange; r < maxRange; r++) + foreach (var tile in self.World.FindTilesInCircle(target, r).Except(searched)) + { + if (check(tile)) + return tile; + + searched.Add(tile); + } // Couldn't find a cell return target; diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj index 353821fbd6..e7eaf027f0 100644 --- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj +++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj @@ -58,6 +58,9 @@ AllRules.ruleset + + ..\thirdparty\FuzzyLogicLibrary.dll + @@ -69,6 +72,7 @@ + @@ -120,6 +124,8 @@ + + diff --git a/OpenRA.Mods.RA/Orders/SetChronoTankDestination.cs b/OpenRA.Mods.RA/Orders/SetChronoTankDestination.cs new file mode 100644 index 0000000000..20c8ee01cf --- /dev/null +++ b/OpenRA.Mods.RA/Orders/SetChronoTankDestination.cs @@ -0,0 +1,57 @@ +#region Copyright & License Information +/* + * Copyright 2007-2011 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. For more information, + * see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Drawing; +using OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.RA.Orders +{ + class SetChronoTankDestination : IOrderGenerator + { + public readonly Actor self; + + public SetChronoTankDestination(Actor self) + { + this.self = self; + } + + public IEnumerable Order(World world, CPos xy, MouseInput mi) + { + if (mi.Button == MouseButton.Left) + { + world.CancelInputMode(); + yield break; + } + + var queued = mi.Modifiers.HasModifier(Modifiers.Shift); + if (world.LocalPlayer.Shroud.IsExplored(xy)) + yield return new Order("ChronoshiftSelf", self, queued) { TargetLocation = xy }; + } + + public void Tick( World world ) { } + public void RenderAfterWorld( WorldRenderer wr, World world ) + { + wr.DrawSelectionBox(self, Color.White); + } + + public void RenderBeforeWorld( WorldRenderer wr, World world ) { } + + public string GetCursor(World world, CPos xy, MouseInput mi) + { + if (!world.LocalPlayer.Shroud.IsExplored(xy)) + return "move-blocked"; + + var movement = self.TraitOrDefault(); + return (movement.CanEnterCell(xy)) ? "chrono-target" : "move-blocked"; + } + } +} diff --git a/OpenRA.Mods.RA/RallyPoint.cs b/OpenRA.Mods.RA/RallyPoint.cs index 7286f0eb1a..504d3ad01b 100755 --- a/OpenRA.Mods.RA/RallyPoint.cs +++ b/OpenRA.Mods.RA/RallyPoint.cs @@ -1,4 +1,4 @@ -#region Copyright & License Information +#region Copyright & License Information /* * Copyright 2007-2011 The OpenRA Developers (see AUTHORS) * This file is part of OpenRA, which is free software. It is made @@ -24,13 +24,15 @@ namespace OpenRA.Mods.RA public class RallyPoint : IIssueOrder, IResolveOrder, ISync { - [Sync] public CPos rallyPoint; + [Sync] + public CPos rallyPoint; public int nearEnough = 1; - public RallyPoint(Actor self, RallyPointInfo info) + public RallyPoint(Actor self) { + var info = self.Info.Traits.Get(); rallyPoint = self.Location + new CVec(info.RallyPoint[0], info.RallyPoint[1]); - self.World.AddFrameEndTask(w => w.Add(new Effects.RallyPoint(self, info.IndicatorPalettePrefix))); + self.World.AddFrameEndTask(w => w.Add(new Effects.RallyPoint(self))); } public IEnumerable Orders diff --git a/mods/cnc/rules/aircraft.yaml b/mods/cnc/rules/aircraft.yaml index 1a0a9d93b9..4c71c76de2 100644 --- a/mods/cnc/rules/aircraft.yaml +++ b/mods/cnc/rules/aircraft.yaml @@ -14,6 +14,7 @@ TRAN: Selectable: Bounds: 41,41 Helicopter: + RearmBuildings: hpad LandWhenIdle: true ROT: 5 Speed: 15 @@ -54,6 +55,7 @@ HELI: Selectable: Bounds: 30,24 Helicopter: + RearmBuildings: hpad ROT: 4 Speed: 20 Health: @@ -100,6 +102,7 @@ ORCA: Selectable: Bounds: 30,24 Helicopter: + RearmBuildings: hpad ROT: 4 Speed: 20 Health: diff --git a/mods/cnc/rules/system.yaml b/mods/cnc/rules/system.yaml index 85349aad31..1551ded728 100644 --- a/mods/cnc/rules/system.yaml +++ b/mods/cnc/rules/system.yaml @@ -49,6 +49,68 @@ Player: htnk: 50% heli: 5% orca: 5% + SquadSize: 15 + HackyAI@Normal: + Name:Normal AI + BuildingGeneralNames: + ConstructionYard: fact + Refinery: proc + Power: nuke,nuk2 + Barracks: pyle,hand + VehiclesFactory: weap,afld + Silo: silo + UnitsGeneralNames: + Mcv: mcv + BuildingLimits: + proc: 4 + pyle: 2 + hand: 2 + hq: 1 + weap: 2 + afld: 2 + hpad: 1 + eye: 1 + tmpl: 1 + fix: 1 + BuildingFractions: + proc: 17% + nuke: 1% + pyle: 2% + hand: 2% + hq: 1% + nuk2: 18% + weap: 5% + afld: 5% + hpad: 4% + gtwr: 5% + gun: 5% + atwr: 9% + obli: 7% + eye: 1% + tmpl: 1% + sam: 7% + silo: 7% + fix: 1% + UnitsToBuild: + e1: 30% #gdi, nod + e2: 30% #gdi + e3: 30% #gdi, nod + e4: 30% #nod + e5: 30% #nod + harv: 1% + bggy: 10% + ftnk: 10% + arty: 40% + bike: 10% + heli: 10% + ltnk: 40% + stnk: 40% + orca: 10% + msam: 50% + htnk: 50% + jeep: 20% + mtnk: 50% + SquadSize: 15 PlayerColorPalette: BasePalette: terrain RemapIndex: 176, 178, 180, 182, 184, 186, 189, 191, 177, 179, 181, 183, 185, 187, 188, 190 diff --git a/mods/d2k/rules/system.yaml b/mods/d2k/rules/system.yaml index 8376d71d8f..54c280292a 100644 --- a/mods/d2k/rules/system.yaml +++ b/mods/d2k/rules/system.yaml @@ -52,6 +52,42 @@ Player: Name:Omnius UnitQueues: Infantry, Vehicle, Armor, Starport RallypointTestBuilding: conyarda + BuildingGeneralNames: + ConstructionYard: conyarda,conyardh,conyardo + Refinery: refa,refh,refo + Power: pwra,pwrh,pwro + VehiclesFactory: lighta,lighth,lighto,heavya,heavyh,heavyo + Silo: siloa, siloh, siloo + UnitsGeneralNames: + Mcv: mcva,mcvh,mcvo + BuildingLimits: + refa: 4 + refh: 4 + refo: 4 + barra: 1 + barrh: 1 + barro: 1 + lighta: 1 + lighth: 1 + lighto: 1 + heavya: 1 + heavyh: 1 + heavyo: 1 + researcha: 1 + researchh: 1 + researcho: 1 + repaira: 1 + repairh: 1 + repairo: 1 + radara: 1 + radaro: 1 + radarh: 1 + hightecha: 1 + hightechh: 1 + hightecho: 1 + palacea: 1 + palaceh: 1 + palaceo: 1 BuildingFractions: refa: 20.1% refh: 20.1% diff --git a/mods/ra/rules/system.yaml b/mods/ra/rules/system.yaml index 36fc421ecf..9ad4280b9d 100644 --- a/mods/ra/rules/system.yaml +++ b/mods/ra/rules/system.yaml @@ -67,24 +67,30 @@ Player: 2tnk: 25% 3tnk: 50% SquadSize: 20 - HackyAI@HardAI: - Name:Hard AI + HackyAI@NormalAI: + Name:Normal AI BuildingFractions: - proc: 30% - powr: 35% + proc: 10% + powr: 1% + apwr: 30% tent: 1% barr: 1% - weap: 1% + weap: 6% + hpad: 4% + spen: 1% + syrd: 1% + afld: 4% pbox.e1: 7% gun: 7% ftur: 10% tsla: 5% - fix: 0.1% - dome: 10% + fix: 1% + dome: 1% agun: 5% sam: 1% atek: 1% stek: 1% + mslo: 1% UnitsToBuild: e1: 50% e3: 10% @@ -95,7 +101,110 @@ Player: 1tnk: 70% 2tnk: 25% 3tnk: 50% + heli: 30% + hind: 30% + mig: 30% + yak: 30% + ss: 10% + msub: 10% + dd: 10% + ca: 10% + pt: 10% + SquadSize: 40 + HackyAI@HardAI: + Name:Hard AI + BuildingFractions: + proc: 30% + powr: 1% + apwr: 30% + tent: 1% + barr: 1% + weap: 3% + hpad: 2% + spen: 1% + syrd: 1% + pbox.e1: 7% + gun: 7% + ftur: 10% + tsla: 5% + fix: 0.1% + dome: 10% + agun: 5% + sam: 1% + atek: 1% + stek: 1% + mslo: 1% + UnitsToBuild: + e1: 50% + e3: 10% + harv: 10% + apc: 30% + jeep: 40% + ftrk: 50% + 1tnk: 70% + 2tnk: 25% + 3tnk: 50% + heli: 30% + hind: 30% + mig: 30% + yak: 30% + ss: 10% + msub: 10% + dd: 10% + ca: 10% + pt: 10% SquadSize: 10 + HackyAI@TestAI: + Name:Test AI + BuildingFractions: + proc: 29% + powr: 1% + apwr: 24% +# tent: 3% +# barr: 3% +# weap: 5% + hpad: 5% + afld: 5% + spen: 1% + syrd: 1% + pbox.e1: 12% + gun: 12% + ftur: 12% + tsla: 12% + fix: 1% + dome: 1% + agun: 5% + sam: 5% + atek: 1% + stek: 1% + mslo: 1% + UnitsToBuild: + e1: 4% + e2: 4% #s + e3: 8% + e4: 4% #s + shok: 3% + harv: 1% + apc: 3% #s + jeep: 5% + ftrk: 8% #s + 1tnk: 9% #a + 2tnk: 20% #a + 3tnk: 10% #s + 4tnk: 14% #s + ttnk: 8% #s + arty: 15% + v2rl: 10% + heli: 7% + hind: 7% + mig: 10% + yak: 1% + ss: 7% + msub: 5% + dd: 8% + ca: 8% + pt: 8% + SquadSize: 15 HackyAI@OptiAI: Name:Eisenhower AI BuildingFractions: diff --git a/thirdparty/FuzzyLogicLibrary.dll b/thirdparty/FuzzyLogicLibrary.dll new file mode 100644 index 0000000000..f1c8742795 Binary files /dev/null and b/thirdparty/FuzzyLogicLibrary.dll differ