From e0b7242f1b0dac2c282aadbb2ecbca5fb6f3d1e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Mail=C3=A4nder?= Date: Wed, 6 Mar 2013 11:17:02 +0100 Subject: [PATCH] adding new ai functional --- OpenRA.Game/Traits/Player/PlayerResources.cs | 8 +- OpenRA.Mods.RA/AI/AttackOrFleeFuzzy.cs | 250 ++++ OpenRA.Mods.RA/AI/BaseBuilder.cs | 8 +- OpenRA.Mods.RA/AI/HackyAI.cs | 1189 ++++++++++++++--- OpenRA.Mods.RA/AI/RushFuzzy.cs | 39 + OpenRA.Mods.RA/AI/StateMachine.cs | 79 ++ OpenRA.Mods.RA/Move/Mobile.cs | 29 +- OpenRA.Mods.RA/OpenRA.Mods.RA.csproj | 6 + .../Orders/SetChronoTankDestination.cs | 57 + OpenRA.Mods.RA/RallyPoint.cs | 10 +- mods/cnc/rules/aircraft.yaml | 3 + mods/cnc/rules/system.yaml | 62 + mods/d2k/rules/system.yaml | 36 + mods/ra/rules/system.yaml | 123 +- thirdparty/FuzzyLogicLibrary.dll | Bin 0 -> 32256 bytes 15 files changed, 1722 insertions(+), 177 deletions(-) create mode 100644 OpenRA.Mods.RA/AI/AttackOrFleeFuzzy.cs create mode 100644 OpenRA.Mods.RA/AI/RushFuzzy.cs create mode 100644 OpenRA.Mods.RA/AI/StateMachine.cs create mode 100644 OpenRA.Mods.RA/Orders/SetChronoTankDestination.cs create mode 100644 thirdparty/FuzzyLogicLibrary.dll 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 0000000000000000000000000000000000000000..f1c8742795ce4a40111c4b8fae58e9045067a72e GIT binary patch literal 32256 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 NuIm5){J-ac{|(*nKNkQ1 literal 0 HcmV?d00001