| | 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 TestDirectAttackJson = |
| | 13 | | @" |
| | 14 | | { |
| | 15 | | ""name"": ""TestDirectAttack"", |
| | 16 | | ""attackBehaviorType"": ""DIRECT_ATTACK"", |
| | 17 | | ""targetPosition"": { |
| | 18 | | ""x"": 0.01, |
| | 19 | | ""y"": 0.0, |
| | 20 | | ""z"": 0.0 |
| | 21 | | }, |
| | 22 | | ""targetVelocity"": { |
| | 23 | | ""x"": 0.0001, |
| | 24 | | ""y"": 0.0, |
| | 25 | | ""z"": 0.0 |
| | 26 | | }, |
| | 27 | | ""targetColliderSize"": { |
| | 28 | | ""x"": 20.0, |
| | 29 | | ""y"": 20.0, |
| | 30 | | ""z"": 20.0 |
| | 31 | | }, |
| | 32 | | ""flightPlan"": { |
| | 33 | | ""type"": ""DistanceToTarget"", |
| | 34 | | ""waypoints"": [ |
| | 35 | | { |
| | 36 | | ""distance"": 10000.0, |
| | 37 | | ""altitude"": 500.0, |
| | 38 | | ""power"": ""MIL"" |
| | 39 | | }, |
| | 40 | | { |
| | 41 | | ""distance"": 5000.0, |
| | 42 | | ""altitude"": 100.0, |
| | 43 | | ""power"": ""MAX"" |
| | 44 | | } |
| | 45 | | ] |
| | 46 | | } |
| | 47 | | } |
| | 48 | | "; |
| | 49 | |
|
| 7 | 50 | | public override void Setup() { |
| 7 | 51 | | base.Setup(); |
| | 52 | | // Write the hard-coded JSON to the file |
| 7 | 53 | | string attackConfigPath = Path.Combine(Application.streamingAssetsPath, |
| | 54 | | "Configs/Behaviors/Attack/test_direct_attack.json"); |
| 7 | 55 | | Directory.CreateDirectory(Path.GetDirectoryName(attackConfigPath)); |
| 7 | 56 | | File.WriteAllText(attackConfigPath, TestDirectAttackJson); |
| | 57 | | // Load configurations using ConfigLoader |
| | 58 | | // Create dynamic configurations for threats |
| 7 | 59 | | var ucavConfig = new DynamicAgentConfig { |
| | 60 | | agent_model = "ucav.json", attack_behavior = "test_direct_attack.json", |
| | 61 | | initial_state = new InitialState { position = new Vector3(2000, 100, 4000), |
| | 62 | | rotation = new Vector3(90, 0, 0), |
| | 63 | | velocity = new Vector3(-50, 0, -100) }, |
| | 64 | | standard_deviation = new StandardDeviation { position = new Vector3(400, 30, 400), |
| | 65 | | velocity = new Vector3(0, 0, 15) }, |
| | 66 | | dynamic_config = |
| | 67 | | new DynamicConfig { launch_config = new LaunchConfig { launch_time = 0 }, |
| | 68 | | sensor_config = |
| | 69 | | new SensorConfig { type = SensorType.IDEAL, frequency = 100 } } |
| | 70 | | }; |
| | 71 | |
|
| 7 | 72 | | var quadcopterConfig = new DynamicAgentConfig { |
| | 73 | | agent_model = "quadcopter.json", attack_behavior = "test_direct_attack.json", |
| | 74 | | initial_state = |
| | 75 | | new InitialState { position = new Vector3(0, 600, 6000), rotation = new Vector3(90, 0, 0), |
| | 76 | | velocity = new Vector3(0, 0, -50) }, |
| | 77 | | standard_deviation = new StandardDeviation { position = new Vector3(1000, 200, 100), |
| | 78 | | velocity = new Vector3(0, 0, 25) }, |
| | 79 | | dynamic_config = |
| | 80 | | new DynamicConfig { launch_config = new LaunchConfig { launch_time = 0 }, |
| | 81 | | sensor_config = |
| | 82 | | new SensorConfig { type = SensorType.IDEAL, frequency = 100 } } |
| | 83 | | }; |
| | 84 | |
|
| 7 | 85 | | Agent threatAgent = CreateTestThreat(ucavConfig); |
| 7 | 86 | | Assert.IsNotNull(threatAgent); |
| 7 | 87 | | Assert.IsTrue(threatAgent is FixedWingThreat); |
| 7 | 88 | | fixedWingThreat = (FixedWingThreat)threatAgent; |
| 7 | 89 | | Assert.IsNotNull(fixedWingThreat); |
| | 90 | |
|
| 7 | 91 | | threatAgent = CreateTestThreat(quadcopterConfig); |
| 7 | 92 | | Assert.IsNotNull(threatAgent); |
| 7 | 93 | | Assert.IsTrue(threatAgent is RotaryWingThreat); |
| 7 | 94 | | rotaryWingThreat = (RotaryWingThreat)threatAgent; |
| 7 | 95 | | Assert.IsNotNull(rotaryWingThreat); |
| 7 | 96 | | } |
| | 97 | |
|
| 7 | 98 | | public override void Teardown() { |
| 7 | 99 | | base.Teardown(); |
| | 100 | | // Delete the attack configuration file |
| 7 | 101 | | string attackConfigPath = Path.Combine(Application.streamingAssetsPath, |
| | 102 | | "Configs/Behaviors/Attack/test_direct_attack.json"); |
| 14 | 103 | | if (File.Exists(attackConfigPath)) { |
| 7 | 104 | | File.Delete(attackConfigPath); |
| 7 | 105 | | } |
| | 106 | |
|
| 14 | 107 | | if (fixedWingThreat != null) { |
| 7 | 108 | | GameObject.DestroyImmediate(fixedWingThreat.gameObject); |
| 7 | 109 | | } |
| | 110 | |
|
| 14 | 111 | | if (rotaryWingThreat != null) { |
| 7 | 112 | | GameObject.DestroyImmediate(rotaryWingThreat.gameObject); |
| 7 | 113 | | } |
| 7 | 114 | | } |
| | 115 | |
|
| | 116 | | [Test] |
| 1 | 117 | | public void TestDirectAttack_LoadedCorrectly() { |
| | 118 | | // Arrange |
| 1 | 119 | | try { |
| | 120 | | // Act |
| 1 | 121 | | DynamicAgentConfig config = new DynamicAgentConfig { |
| | 122 | | agent_model = "ucav.json", attack_behavior = "test_direct_attack.json", |
| | 123 | | initial_state = new InitialState(), standard_deviation = new StandardDeviation(), |
| | 124 | | dynamic_config = new DynamicConfig() |
| | 125 | | }; |
| | 126 | |
|
| 1 | 127 | | Threat threat = CreateTestThreat(config); |
| | 128 | |
|
| | 129 | | // Assert |
| 1 | 130 | | Assert.IsNotNull(threat, "Threat should not be null"); |
| | 131 | |
|
| 1 | 132 | | AttackBehavior attackBehavior = GetPrivateField<AttackBehavior>(threat, "_attackBehavior"); |
| 1 | 133 | | Assert.IsNotNull(attackBehavior, "Attack behavior should not be null"); |
| 1 | 134 | | Assert.AreEqual("TestDirectAttack", attackBehavior.name); |
| 1 | 135 | | Assert.AreEqual(AttackBehavior.AttackBehaviorType.DIRECT_ATTACK, |
| | 136 | | attackBehavior.attackBehaviorType); |
| | 137 | |
|
| 1 | 138 | | Assert.IsTrue(attackBehavior is DirectAttackBehavior, |
| | 139 | | "Attack behavior should be a DirectAttackBehavior"); |
| 1 | 140 | | DirectAttackBehavior directAttackBehavior = (DirectAttackBehavior)attackBehavior; |
| | 141 | |
|
| 1 | 142 | | Vector3 targetPosition = directAttackBehavior.targetPosition; |
| 1 | 143 | | Assert.AreEqual(new Vector3(0.01f, 0, 0), targetPosition); |
| | 144 | |
|
| 1 | 145 | | DTTFlightPlan flightPlan = directAttackBehavior.flightPlan; |
| 1 | 146 | | Assert.IsNotNull(flightPlan, "Flight plan should not be null"); |
| 1 | 147 | | Assert.AreEqual("DistanceToTarget", flightPlan.type); |
| | 148 | |
|
| 1 | 149 | | List<DTTWaypoint> dttWaypoints = flightPlan.waypoints; |
| 1 | 150 | | Assert.IsNotNull(dttWaypoints, "Waypoints should not be null"); |
| 1 | 151 | | Assert.AreEqual(2, dttWaypoints.Count, "There should be 2 waypoints"); |
| | 152 | |
|
| 1 | 153 | | Assert.AreEqual(5000f, dttWaypoints[0].distance); |
| 1 | 154 | | Assert.AreEqual(100f, dttWaypoints[0].altitude); |
| 1 | 155 | | Assert.AreEqual(PowerSetting.MAX, dttWaypoints[0].power); |
| | 156 | |
|
| 1 | 157 | | Assert.AreEqual(10000f, dttWaypoints[1].distance); |
| 1 | 158 | | Assert.AreEqual(500f, dttWaypoints[1].altitude); |
| 1 | 159 | | Assert.AreEqual(PowerSetting.MIL, dttWaypoints[1].power); |
| | 160 | |
|
| | 161 | | // Check targetVelocity |
| 1 | 162 | | Vector3 targetVelocity = directAttackBehavior.targetVelocity; |
| 1 | 163 | | Assert.AreEqual(new Vector3(0.0001f, 0f, 0f), targetVelocity, |
| | 164 | | "Target velocity should match the config"); |
| | 165 | |
|
| | 166 | | // Check targetColliderSize |
| 1 | 167 | | Vector3 targetColliderSize = directAttackBehavior.targetColliderSize; |
| 1 | 168 | | Assert.AreEqual(new Vector3(20f, 20f, 20f), targetColliderSize, |
| | 169 | | "Target collider size should match the config"); |
| | 170 | |
|
| | 171 | | // Check targetPosition (more precise check) |
| 1 | 172 | | Assert.AreEqual(0.01f, targetPosition.x, 0.0001f, "Target position X should be 0.01"); |
| 1 | 173 | | Assert.AreEqual(0f, targetPosition.y, 0.0001f, "Target position Y should be 0"); |
| 1 | 174 | | Assert.AreEqual(0f, targetPosition.z, 0.0001f, "Target position Z should be 0"); |
| | 175 | |
|
| | 176 | | // Clean up |
| 1 | 177 | | GameObject.DestroyImmediate(simManager.gameObject); |
| 1 | 178 | | } catch (AssertionException e) { |
| 0 | 179 | | throw new AssertionException( |
| | 180 | | e.Message + "\n" + "This test likely failed because you have edited " + |
| | 181 | | "The test string at the top of the test. Please update the test with the new values.\n" + |
| | 182 | | "If you need to change the test values, please update the test string at the top of the test."); |
| | 183 | | } |
| 1 | 184 | | } |
| | 185 | |
|
| | 186 | | [Test] |
| 1 | 187 | | public void Threat_IsNotAssignable() { |
| 1 | 188 | | Assert.IsFalse(fixedWingThreat.IsAssignable()); |
| 1 | 189 | | Assert.IsFalse(rotaryWingThreat.IsAssignable()); |
| 1 | 190 | | } |
| | 191 | |
|
| | 192 | | [Test] |
| 1 | 193 | | public void FixedWingThreat_CalculateAccelerationInput_RespectsMaxForwardAcceleration() { |
| 1 | 194 | | SetPrivateField(fixedWingThreat, "_currentWaypoint", Vector3.one * 1000f); |
| 1 | 195 | | Vector3 acceleration = |
| | 196 | | InvokePrivateMethod<Vector3>(fixedWingThreat, "CalculateAccelerationInput"); |
| 1 | 197 | | float maxForwardAcceleration = |
| | 198 | | InvokePrivateMethod<float>(fixedWingThreat, "CalculateMaxForwardAcceleration"); |
| | 199 | | const float epsilon = 1e-5f; |
| 1 | 200 | | Assert.LessOrEqual((Vector3.Project(acceleration, fixedWingThreat.transform.forward)).magnitude, |
| | 201 | | maxForwardAcceleration + epsilon); |
| 1 | 202 | | } |
| | 203 | |
|
| | 204 | | [Test] |
| 1 | 205 | | public void FixedWingThreat_CalculateAccelerationInput_RespectsMaxNormalAcceleration() { |
| 1 | 206 | | SetPrivateField(fixedWingThreat, "_currentWaypoint", Vector3.one * 1000f); |
| 1 | 207 | | Vector3 acceleration = |
| | 208 | | InvokePrivateMethod<Vector3>(fixedWingThreat, "CalculateAccelerationInput"); |
| 1 | 209 | | float maxNormalAcceleration = |
| | 210 | | InvokePrivateMethod<float>(fixedWingThreat, "CalculateMaxNormalAcceleration"); |
| | 211 | | const float epsilon = 1e-5f; |
| 1 | 212 | | Assert.LessOrEqual( |
| | 213 | | acceleration.magnitude, maxNormalAcceleration + epsilon, |
| | 214 | | $"Acceleration magnitude {acceleration.magnitude} should be less than or equal to max normal acceleration {maxNo |
| 1 | 215 | | } |
| | 216 | |
|
| | 217 | | [Test] |
| 1 | 218 | | public void RotaryWingThreat_CalculateAccelerationToWaypoint_RespectsMaxForwardAcceleration() { |
| 1 | 219 | | SetPrivateField(rotaryWingThreat, "_currentWaypoint", Vector3.one * 1000f); |
| 1 | 220 | | Vector3 acceleration = |
| | 221 | | InvokePrivateMethod<Vector3>(rotaryWingThreat, "CalculateAccelerationToWaypoint"); |
| 1 | 222 | | float maxForwardAcceleration = |
| | 223 | | InvokePrivateMethod<float>(rotaryWingThreat, "CalculateMaxForwardAcceleration"); |
| | 224 | | const float epsilon = 1e-5f; |
| 1 | 225 | | Assert.LessOrEqual((Vector3.Project(acceleration, fixedWingThreat.transform.forward)).magnitude, |
| | 226 | | maxForwardAcceleration + epsilon); |
| 1 | 227 | | } |
| | 228 | |
|
| | 229 | | [Test] |
| 1 | 230 | | public void RotaryWingThreat_CalculateAccelerationToWaypoint_RespectsMaxNormalAcceleration() { |
| 1 | 231 | | SetPrivateField(rotaryWingThreat, "_currentWaypoint", Vector3.one * 1000f); |
| 1 | 232 | | Vector3 acceleration = |
| | 233 | | InvokePrivateMethod<Vector3>(rotaryWingThreat, "CalculateAccelerationToWaypoint"); |
| 1 | 234 | | float maxNormalAcceleration = |
| | 235 | | InvokePrivateMethod<float>(rotaryWingThreat, "CalculateMaxNormalAcceleration"); |
| | 236 | | const float epsilon = 1e-5f; |
| | 237 | | // Calculate the normal component of acceleration |
| 1 | 238 | | Vector3 forwardComponent = Vector3.Project(acceleration, rotaryWingThreat.transform.forward); |
| 1 | 239 | | Vector3 normalComponent = acceleration - forwardComponent; |
| | 240 | |
|
| 1 | 241 | | Assert.LessOrEqual( |
| | 242 | | normalComponent.magnitude, maxNormalAcceleration + epsilon, |
| | 243 | | $"Normal acceleration magnitude {normalComponent.magnitude} should be less than or equal to max normal accelerat |
| 1 | 244 | | } |
| | 245 | |
|
| | 246 | | private class MockAttackBehavior : AttackBehavior { |
| | 247 | | private Vector3 waypoint; |
| | 248 | | private PowerSetting powerSetting; |
| | 249 | |
|
| 0 | 250 | | public MockAttackBehavior(Vector3 waypoint, PowerSetting powerSetting) { |
| 0 | 251 | | this.waypoint = waypoint; |
| 0 | 252 | | this.powerSetting = powerSetting; |
| 0 | 253 | | this.name = "MockAttackBehavior"; |
| 0 | 254 | | } |
| | 255 | |
|
| | 256 | | public override (Vector3, PowerSetting) |
| 0 | 257 | | GetNextWaypoint(Vector3 currentPosition, Vector3 targetPosition) { |
| 0 | 258 | | return (waypoint, powerSetting); |
| 0 | 259 | | } |
| | 260 | | } |
| | 261 | |
|
| | 262 | | [Test] |
| 1 | 263 | | public void RotaryWingThreat_CalculateAccelerationToWaypoint_ComputesCorrectly() { |
| | 264 | | // Arrange |
| 1 | 265 | | Vector3 initialPosition = new Vector3(0, 0, 0); |
| 1 | 266 | | Vector3 waypoint = new Vector3(1000, 0, 0); |
| 1 | 267 | | Vector3 initialVelocity = new Vector3(0, 0, 0); |
| 1 | 268 | | float desiredSpeed = 50f; |
| | 269 | |
|
| 1 | 270 | | rotaryWingThreat.SetPosition(initialPosition); |
| 1 | 271 | | SetPrivateField(rotaryWingThreat, "_currentWaypoint", waypoint); |
| 1 | 272 | | rotaryWingThreat.SetVelocity(initialVelocity); |
| 1 | 273 | | SetPrivateField(rotaryWingThreat, "_currentPowerSetting", PowerSetting.MIL); |
| | 274 | |
|
| | 275 | | // Assume PowerTableLookup returns 50 for PowerSetting.MIL |
| | 276 | | // Assert that its true using PowerTableLookup |
| 1 | 277 | | float powerSetting = |
| | 278 | | InvokePrivateMethod<float>(rotaryWingThreat, "PowerTableLookup", PowerSetting.MIL); |
| 1 | 279 | | Assert.AreEqual(desiredSpeed, powerSetting); |
| | 280 | |
|
| | 281 | | // Act |
| 1 | 282 | | Vector3 accelerationInput = |
| | 283 | | InvokePrivateMethod<Vector3>(rotaryWingThreat, "CalculateAccelerationToWaypoint"); |
| | 284 | |
|
| | 285 | | // Assert |
| 1 | 286 | | Vector3 toWaypoint = waypoint - initialPosition; |
| 1 | 287 | | Vector3 expectedAccelerationDir = toWaypoint.normalized; |
| 1 | 288 | | float expectedAccelerationMag = desiredSpeed / (float)Time.fixedDeltaTime; |
| 1 | 289 | | Vector3 expectedAcceleration = expectedAccelerationDir * expectedAccelerationMag; |
| | 290 | |
|
| | 291 | | // Decompose acceleration into forward and normal acceleration components |
| 1 | 292 | | Vector3 forwardAcceleration = |
| | 293 | | Vector3.Project(expectedAcceleration, rotaryWingThreat.transform.forward); |
| 1 | 294 | | Vector3 normalAcceleration = expectedAcceleration - forwardAcceleration; |
| | 295 | |
|
| | 296 | | // Limit acceleration magnitude |
| 1 | 297 | | float maxForwardAcceleration = |
| | 298 | | InvokePrivateMethod<float>(rotaryWingThreat, "CalculateMaxForwardAcceleration"); |
| 1 | 299 | | forwardAcceleration = Vector3.ClampMagnitude(forwardAcceleration, maxForwardAcceleration); |
| 1 | 300 | | float maxNormalAcceleration = |
| | 301 | | InvokePrivateMethod<float>(rotaryWingThreat, "CalculateMaxNormalAcceleration"); |
| 1 | 302 | | normalAcceleration = Vector3.ClampMagnitude(normalAcceleration, maxNormalAcceleration); |
| 1 | 303 | | expectedAcceleration = forwardAcceleration + normalAcceleration; |
| | 304 | |
|
| 1 | 305 | | Assert.AreEqual(expectedAcceleration.magnitude, accelerationInput.magnitude, 0.1f, |
| | 306 | | "Acceleration magnitude should match expected"); |
| 1 | 307 | | Assert.AreEqual(expectedAcceleration.normalized, accelerationInput.normalized, |
| | 308 | | "Acceleration direction should be towards waypoint"); |
| 1 | 309 | | } |
| | 310 | | } |