| | 1 | | using System; |
| | 2 | | using NUnit.Framework; |
| | 3 | | using UnityEngine; |
| | 4 | | using System.Collections.Generic; |
| | 5 | | using System.Linq; |
| | 6 | | using System.IO; |
| | 7 | |
|
| | 8 | | public class ThreatTests : AgentTestBase { |
| | 9 | | private FixedWingThreat _fixedWingThreat; |
| | 10 | | private RotaryWingThreat _rotaryWingThreat; |
| | 11 | |
|
| | 12 | | private const string TestDirectAttackPbtxt = |
| | 13 | | @" |
| | 14 | | name: ""Test Direct Attack"" |
| | 15 | | type: DIRECT_ATTACK |
| | 16 | | flight_plan { |
| | 17 | | type: DISTANCE_TO_TARGET |
| | 18 | | waypoints { |
| | 19 | | distance: 10000 |
| | 20 | | altitude: 500 |
| | 21 | | power: MIL |
| | 22 | | } |
| | 23 | | waypoints { |
| | 24 | | distance: 5000 |
| | 25 | | altitude: 100 |
| | 26 | | power: MAX |
| | 27 | | } |
| | 28 | | } |
| | 29 | | "; |
| | 30 | |
|
| 0 | 31 | | public override void Setup() { |
| 0 | 32 | | base.Setup(); |
| | 33 | | // Write the hard-coded attack behavior to a file. |
| 0 | 34 | | string attackConfigPath = |
| | 35 | | ConfigLoader.GetStreamingAssetsFilePath("Configs/Attacks/test_direct_attack.pbtxt"); |
| 0 | 36 | | Directory.CreateDirectory(Path.GetDirectoryName(attackConfigPath)); |
| 0 | 37 | | File.WriteAllText(attackConfigPath, TestDirectAttackPbtxt); |
| | 38 | |
|
| 0 | 39 | | var ucavConfig = new Configs.AgentConfig() { |
| | 40 | | ConfigFile = "ucav.pbtxt", AttackBehaviorConfigFile = "test_direct_attack.pbtxt", |
| | 41 | | InitialState = |
| | 42 | | new Simulation.State() { |
| | 43 | | Position = new Simulation.CartesianCoordinates() { X = 2000, Y = 100, Z = 4000 }, |
| | 44 | | Velocity = new Simulation.CartesianCoordinates() { X = -50, Y = 0, Z = -100 }, |
| | 45 | | }, |
| | 46 | | StandardDeviation = |
| | 47 | | new Simulation.State() { |
| | 48 | | Position = new Simulation.CartesianCoordinates() { X = 400, Y = 300, Z = 400 }, |
| | 49 | | Velocity = new Simulation.CartesianCoordinates() { X = 0, Y = 0, Z = 15 }, |
| | 50 | | }, |
| | 51 | | DynamicConfig = |
| | 52 | | new Configs.DynamicConfig() { |
| | 53 | | SensorConfig = |
| | 54 | | new Simulation.SensorConfig { |
| | 55 | | Type = Simulation.SensorType.Ideal, |
| | 56 | | Frequency = 100, |
| | 57 | | }, |
| | 58 | | } |
| | 59 | | }; |
| | 60 | |
|
| 0 | 61 | | var quadcopterConfig = new Configs.AgentConfig() { |
| | 62 | | ConfigFile = "quadcopter.pbtxt", AttackBehaviorConfigFile = "test_direct_attack.pbtxt", |
| | 63 | | InitialState = |
| | 64 | | new Simulation.State() { |
| | 65 | | Position = new Simulation.CartesianCoordinates() { X = 0, Y = 600, Z = 6000 }, |
| | 66 | | Velocity = new Simulation.CartesianCoordinates() { X = 0, Y = 0, Z = -50 }, |
| | 67 | | }, |
| | 68 | | StandardDeviation = |
| | 69 | | new Simulation.State() { |
| | 70 | | Position = new Simulation.CartesianCoordinates() { X = 100, Y = 200, Z = 100 }, |
| | 71 | | Velocity = new Simulation.CartesianCoordinates() { X = 0, Y = 0, Z = 25 }, |
| | 72 | | }, |
| | 73 | | DynamicConfig = |
| | 74 | | new Configs.DynamicConfig() { |
| | 75 | | SensorConfig = |
| | 76 | | new Simulation.SensorConfig { |
| | 77 | | Type = Simulation.SensorType.Ideal, |
| | 78 | | Frequency = 100, |
| | 79 | | }, |
| | 80 | | } |
| | 81 | | }; |
| | 82 | |
|
| 0 | 83 | | Agent threatAgent = CreateTestThreat(ucavConfig); |
| 0 | 84 | | Assert.IsNotNull(threatAgent); |
| 0 | 85 | | Assert.IsTrue(threatAgent is FixedWingThreat); |
| 0 | 86 | | _fixedWingThreat = (FixedWingThreat)threatAgent; |
| 0 | 87 | | Assert.IsNotNull(_fixedWingThreat); |
| | 88 | |
|
| 0 | 89 | | threatAgent = CreateTestThreat(quadcopterConfig); |
| 0 | 90 | | Assert.IsNotNull(threatAgent); |
| 0 | 91 | | Assert.IsTrue(threatAgent is RotaryWingThreat); |
| 0 | 92 | | _rotaryWingThreat = (RotaryWingThreat)threatAgent; |
| 0 | 93 | | Assert.IsNotNull(_rotaryWingThreat); |
| 0 | 94 | | } |
| | 95 | |
|
| 0 | 96 | | public override void Teardown() { |
| 0 | 97 | | base.Teardown(); |
| | 98 | | // Delete the attack configuration file. |
| 0 | 99 | | string attackConfigPath = |
| | 100 | | ConfigLoader.GetStreamingAssetsFilePath("Configs/Attacks/test_direct_attack.pbtxt"); |
| 0 | 101 | | if (File.Exists(attackConfigPath)) { |
| 0 | 102 | | File.Delete(attackConfigPath); |
| 0 | 103 | | } |
| | 104 | |
|
| 0 | 105 | | if (_fixedWingThreat != null) { |
| 0 | 106 | | GameObject.DestroyImmediate(_fixedWingThreat.gameObject); |
| 0 | 107 | | } |
| | 108 | |
|
| 0 | 109 | | if (_rotaryWingThreat != null) { |
| 0 | 110 | | GameObject.DestroyImmediate(_rotaryWingThreat.gameObject); |
| 0 | 111 | | } |
| 0 | 112 | | } |
| | 113 | |
|
| | 114 | | [Test] |
| 0 | 115 | | public void Threat_IsNotAssignable() { |
| 0 | 116 | | Assert.IsFalse(_fixedWingThreat.IsAssignable()); |
| 0 | 117 | | Assert.IsFalse(_rotaryWingThreat.IsAssignable()); |
| 0 | 118 | | } |
| | 119 | |
|
| | 120 | | [Test] |
| 0 | 121 | | public void FixedWingThreat_CalculateAccelerationInput_RespectsMaxForwardAcceleration() { |
| 0 | 122 | | SetPrivateField(_fixedWingThreat, "_currentWaypoint", Vector3.one * 1000f); |
| 0 | 123 | | Vector3 acceleration = |
| | 124 | | InvokePrivateMethod<Vector3>(_fixedWingThreat, "CalculateAccelerationInput"); |
| 0 | 125 | | float maxForwardAcceleration = _fixedWingThreat.CalculateMaxForwardAcceleration(); |
| | 126 | | const float epsilon = 1e-5f; |
| 0 | 127 | | Assert.LessOrEqual(Vector3.Project(acceleration, _fixedWingThreat.transform.forward).magnitude, |
| | 128 | | maxForwardAcceleration + epsilon); |
| 0 | 129 | | } |
| | 130 | |
|
| | 131 | | [Test] |
| 0 | 132 | | public void FixedWingThreat_CalculateAccelerationInput_RespectsMaxNormalAcceleration() { |
| 0 | 133 | | SetPrivateField(_fixedWingThreat, "_currentWaypoint", Vector3.one * 1000f); |
| 0 | 134 | | Vector3 acceleration = |
| | 135 | | InvokePrivateMethod<Vector3>(_fixedWingThreat, "CalculateAccelerationInput"); |
| 0 | 136 | | float maxNormalAcceleration = _fixedWingThreat.CalculateMaxNormalAcceleration(); |
| | 137 | | const float epsilon = 1e-5f; |
| 0 | 138 | | Assert.LessOrEqual( |
| | 139 | | acceleration.magnitude, maxNormalAcceleration + epsilon, |
| | 140 | | $"Acceleration magnitude {acceleration.magnitude} should be less than or equal to the maximum normal acceleratio |
| 0 | 141 | | } |
| | 142 | |
|
| | 143 | | [Test] |
| 0 | 144 | | public void RotaryWingThreat_CalculateAccelerationToWaypoint_RespectsMaxForwardAcceleration() { |
| 0 | 145 | | SetPrivateField(_rotaryWingThreat, "_currentWaypoint", Vector3.one * 1000f); |
| 0 | 146 | | Vector3 acceleration = |
| | 147 | | InvokePrivateMethod<Vector3>(_rotaryWingThreat, "CalculateAccelerationToWaypoint"); |
| 0 | 148 | | float maxForwardAcceleration = _rotaryWingThreat.CalculateMaxForwardAcceleration(); |
| | 149 | | const float epsilon = 1e-5f; |
| 0 | 150 | | Assert.LessOrEqual(Vector3.Project(acceleration, _fixedWingThreat.transform.forward).magnitude, |
| | 151 | | maxForwardAcceleration + epsilon); |
| 0 | 152 | | } |
| | 153 | |
|
| | 154 | | [Test] |
| 0 | 155 | | public void RotaryWingThreat_CalculateAccelerationToWaypoint_RespectsMaxNormalAcceleration() { |
| 0 | 156 | | SetPrivateField(_rotaryWingThreat, "_currentWaypoint", Vector3.one * 1000f); |
| 0 | 157 | | Vector3 acceleration = |
| | 158 | | InvokePrivateMethod<Vector3>(_rotaryWingThreat, "CalculateAccelerationToWaypoint"); |
| 0 | 159 | | float maxNormalAcceleration = _rotaryWingThreat.CalculateMaxNormalAcceleration(); |
| | 160 | | const float epsilon = 1e-5f; |
| | 161 | | // Calculate the normal acceleration. |
| 0 | 162 | | Vector3 forwardComponent = Vector3.Project(acceleration, _rotaryWingThreat.transform.forward); |
| 0 | 163 | | Vector3 normalComponent = acceleration - forwardComponent; |
| | 164 | |
|
| 0 | 165 | | Assert.LessOrEqual( |
| | 166 | | normalComponent.magnitude, maxNormalAcceleration + epsilon, |
| | 167 | | $"Normal acceleration magnitude {normalComponent.magnitude} should be less than or equal to the maximum normal a |
| 0 | 168 | | } |
| | 169 | |
|
| | 170 | | [Test] |
| 0 | 171 | | public void RotaryWingThreat_CalculateAccelerationToWaypoint_ComputesCorrectly() { |
| 0 | 172 | | Vector3 initialPosition = new Vector3(0, 0, 0); |
| 0 | 173 | | Vector3 waypoint = new Vector3(1000, 0, 0); |
| 0 | 174 | | Vector3 initialVelocity = new Vector3(0, 0, 0); |
| 0 | 175 | | float desiredSpeed = 50f; |
| | 176 | |
|
| 0 | 177 | | _rotaryWingThreat.SetPosition(initialPosition); |
| 0 | 178 | | SetPrivateField(_rotaryWingThreat, "_currentWaypoint", waypoint); |
| 0 | 179 | | _rotaryWingThreat.SetVelocity(initialVelocity); |
| 0 | 180 | | SetPrivateField(_rotaryWingThreat, "_currentPower", Configs.Power.Mil); |
| | 181 | |
|
| | 182 | | // Assume that the lookup power table returns 50 for MIL. |
| 0 | 183 | | float power = |
| | 184 | | InvokePrivateMethod<float>(_rotaryWingThreat, "LookupPowerTable", Configs.Power.Mil); |
| 0 | 185 | | Assert.AreEqual(desiredSpeed, power); |
| | 186 | |
|
| 0 | 187 | | Vector3 accelerationInput = |
| | 188 | | InvokePrivateMethod<Vector3>(_rotaryWingThreat, "CalculateAccelerationToWaypoint"); |
| | 189 | |
|
| 0 | 190 | | Vector3 toWaypoint = waypoint - initialPosition; |
| 0 | 191 | | Vector3 expectedAccelerationDir = toWaypoint.normalized; |
| 0 | 192 | | float expectedAccelerationMag = desiredSpeed / (float)Time.fixedDeltaTime; |
| 0 | 193 | | Vector3 expectedAcceleration = expectedAccelerationDir * expectedAccelerationMag; |
| | 194 | |
|
| | 195 | | // Decompose acceleration into forward and normal components. |
| 0 | 196 | | Vector3 forwardAcceleration = |
| | 197 | | Vector3.Project(expectedAcceleration, _rotaryWingThreat.transform.forward); |
| 0 | 198 | | Vector3 normalAcceleration = expectedAcceleration - forwardAcceleration; |
| | 199 | |
|
| | 200 | | // Limit the acceleration magnitude. |
| 0 | 201 | | float maxForwardAcceleration = _rotaryWingThreat.CalculateMaxNormalAcceleration(); |
| 0 | 202 | | forwardAcceleration = Vector3.ClampMagnitude(forwardAcceleration, maxForwardAcceleration); |
| 0 | 203 | | float maxNormalAcceleration = _rotaryWingThreat.CalculateMaxNormalAcceleration(); |
| 0 | 204 | | normalAcceleration = Vector3.ClampMagnitude(normalAcceleration, maxNormalAcceleration); |
| 0 | 205 | | expectedAcceleration = forwardAcceleration + normalAcceleration; |
| | 206 | |
|
| 0 | 207 | | Assert.AreEqual(expectedAcceleration.magnitude, accelerationInput.magnitude, 0.1f, |
| | 208 | | "Acceleration magnitude should match expected."); |
| 0 | 209 | | Assert.AreEqual(expectedAcceleration.normalized, accelerationInput.normalized, |
| | 210 | | "Acceleration direction should be towards waypoint."); |
| 0 | 211 | | } |
| | 212 | |
|
| | 213 | | [Test] |
| 0 | 214 | | public void AttackBehavior_LoadedCorrectly() { |
| 0 | 215 | | try { |
| 0 | 216 | | AttackBehavior attackBehavior = |
| | 217 | | GetPrivateField<AttackBehavior>(_fixedWingThreat, "_attackBehavior"); |
| 0 | 218 | | Assert.IsNotNull(attackBehavior, "Attack behavior should not be null."); |
| 0 | 219 | | Assert.AreEqual("Test Direct Attack", attackBehavior.Name); |
| 0 | 220 | | Assert.AreEqual(Configs.AttackType.DirectAttack, attackBehavior.Type); |
| | 221 | |
|
| 0 | 222 | | Assert.IsTrue(attackBehavior is DirectAttackBehavior, |
| | 223 | | "Attack behavior should be a DirectAttackBehavior."); |
| 0 | 224 | | DirectAttackBehavior directAttackBehavior = (DirectAttackBehavior)attackBehavior; |
| | 225 | |
|
| 0 | 226 | | Assert.IsNotNull(attackBehavior.FlightPlan, "Flight plan should not be null."); |
| 0 | 227 | | Assert.AreEqual(Configs.AttackBehaviorConfig.Types.FlightPlanType.DistanceToTarget, |
| | 228 | | attackBehavior.FlightPlan.Type); |
| | 229 | |
|
| 0 | 230 | | List<Waypoint> waypoints = attackBehavior.FlightPlan.Waypoints; |
| 0 | 231 | | Assert.IsNotNull(waypoints, "Waypoints should not be null."); |
| 0 | 232 | | Assert.AreEqual(2, waypoints.Count, "There should be 2 waypoints."); |
| | 233 | |
|
| 0 | 234 | | Assert.AreEqual(10000f, waypoints[0].Distance); |
| 0 | 235 | | Assert.AreEqual(500f, waypoints[0].Altitude); |
| 0 | 236 | | Assert.AreEqual(Configs.Power.Mil, waypoints[0].Power); |
| | 237 | |
|
| 0 | 238 | | Assert.AreEqual(5000f, waypoints[1].Distance); |
| 0 | 239 | | Assert.AreEqual(100f, waypoints[1].Altitude); |
| 0 | 240 | | Assert.AreEqual(Configs.Power.Max, waypoints[1].Power); |
| | 241 | |
|
| 0 | 242 | | GameObject.DestroyImmediate(_simManager.gameObject); |
| 0 | 243 | | } catch (AssertionException e) { |
| 0 | 244 | | throw new AssertionException( |
| | 245 | | e.Message + "\n" + "This test likely failed because you have edited " + |
| | 246 | | "the test string at the top of the test. Please update the test with the new values.\n" + |
| | 247 | | "If you need to change the test values, please update the test string at the top of the test."); |
| | 248 | | } |
| 0 | 249 | | } |
| | 250 | | } |