From e15b70fafce668174b1bffff30df3db2f07a33e9 Mon Sep 17 00:00:00 2001 From: Daniel Lovell Date: Sun, 29 Sep 2024 23:18:40 -0700 Subject: [PATCH] Assignment system overhaul to fix many bugs --- Assets/Scripts/Agent.cs | 24 ++- Assets/Scripts/Assignment/Assignment.cs | 40 +++-- .../Assignment/RoundRobinAssignment.cs | 68 +++++---- Assets/Scripts/Assignment/ThreatAssignment.cs | 76 +++++----- Assets/Scripts/IADS/ThreatData.cs | 48 ++++++ Assets/Scripts/IADS/ThreatData.cs.meta | 2 + Assets/Scripts/Interceptor.cs | 8 +- Assets/Scripts/Interceptors/Micromissile.cs | 2 +- Assets/Scripts/MicromissileAssembly.asmdef | 16 ++ .../Scripts/MicromissileAssembly.asmdef.meta | 7 + Assets/Scripts/Monitor.cs | 2 +- Assets/Scripts/SimManager.cs | 134 ++++++++++------- Assets/{ss.meta => Tests.meta} | 2 +- Assets/Tests/SanityTest.cs | 25 ++++ Assets/Tests/SanityTest.cs.meta | 2 + Assets/Tests/Tests.asmdef | 24 +++ Assets/Tests/Tests.asmdef.meta | 7 + Assets/Tests/ThreatAssignmentTests.cs | 138 ++++++++++++++++++ Assets/Tests/ThreatAssignmentTests.cs.meta | 2 + ProjectSettings/SceneTemplateSettings.json | 5 + 20 files changed, 467 insertions(+), 165 deletions(-) create mode 100644 Assets/Scripts/IADS/ThreatData.cs create mode 100644 Assets/Scripts/IADS/ThreatData.cs.meta create mode 100644 Assets/Scripts/MicromissileAssembly.asmdef create mode 100644 Assets/Scripts/MicromissileAssembly.asmdef.meta rename Assets/{ss.meta => Tests.meta} (77%) create mode 100644 Assets/Tests/SanityTest.cs create mode 100644 Assets/Tests/SanityTest.cs.meta create mode 100644 Assets/Tests/Tests.asmdef create mode 100644 Assets/Tests/Tests.asmdef.meta create mode 100644 Assets/Tests/ThreatAssignmentTests.cs create mode 100644 Assets/Tests/ThreatAssignmentTests.cs.meta diff --git a/Assets/Scripts/Agent.cs b/Assets/Scripts/Agent.cs index 218dcad..251cb13 100644 --- a/Assets/Scripts/Agent.cs +++ b/Assets/Scripts/Agent.cs @@ -33,12 +33,12 @@ public abstract class Agent : MonoBehaviour { protected StaticConfig _staticConfig; // Define delegates - public delegate void AgentHitEventHandler(Agent agent); - public delegate void AgentMissEventHandler(Agent agent); + public delegate void InterceptHitEventHandler(Interceptor interceptor, Threat target); + public delegate void InterceptMissEventHandler(Interceptor interceptor, Threat target); // Define events - public event AgentHitEventHandler OnAgentHit; - public event AgentMissEventHandler OnAgentMiss; + public event InterceptHitEventHandler OnInterceptHit; + public event InterceptMissEventHandler OnInterceptMiss; public void SetFlightPhase(FlightPhase flightPhase) { Debug.Log( @@ -105,16 +105,24 @@ public abstract class Agent : MonoBehaviour { } // Mark the agent as having hit the target or been hit. - public void MarkAsHit() { + public void HandleInterceptHit() { _isHit = true; - OnAgentHit?.Invoke(this); + if (this is Interceptor interceptor && _target is Threat threat) { + OnInterceptHit?.Invoke(interceptor, threat); + } else if (this is Threat threatAgent && _target is Interceptor interceptorTarget) { + OnInterceptHit?.Invoke(interceptorTarget, threatAgent); + } TerminateAgent(); } - public void MarkAsMiss() { + public void HandleInterceptMiss() { _isMiss = true; if (_target != null) { - OnAgentMiss?.Invoke(this); + if (this is Interceptor interceptor && _target is Threat threat) { + OnInterceptMiss?.Invoke(interceptor, threat); + } else if (this is Threat threatAgent && _target is Interceptor interceptorTarget) { + OnInterceptMiss?.Invoke(interceptorTarget, threatAgent); + } _target = null; } TerminateAgent(); diff --git a/Assets/Scripts/Assignment/Assignment.cs b/Assets/Scripts/Assignment/Assignment.cs index 427c356..7c5709c 100644 --- a/Assets/Scripts/Assignment/Assignment.cs +++ b/Assets/Scripts/Assignment/Assignment.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using UnityEngine; +using System.Linq; +using System.Diagnostics.Contracts; // The assignment class is an interface for assigning a threat to each interceptor. public interface IAssignment { @@ -8,39 +11,30 @@ public interface IAssignment { // The first element corresponds to the interceptor index, and the second element // corresponds to the threat index. public struct AssignmentItem { - public int InterceptorIndex; - public int ThreatIndex; + public Interceptor Interceptor; + public Threat Threat; - public AssignmentItem(int missileIndex, int threatIndex) { - InterceptorIndex = missileIndex; - ThreatIndex = threatIndex; + public AssignmentItem(Interceptor interceptor, Threat threat) { + Interceptor = interceptor; + Threat = threat; } } // A list containing the interceptor-target assignments. // Assign a target to each interceptor that has not been assigned a target yet. - public abstract IEnumerable Assign(List missiles, List targets); + [Pure] + public abstract IEnumerable Assign(in IReadOnlyList interceptors, in IReadOnlyList threatTable); // Get the list of assignable interceptor indices. - protected static List GetAssignableInterceptorIndices(List missiles) { - List assignableInterceptorIndices = new List(); - for (int missileIndex = 0; missileIndex < missiles.Count; missileIndex++) { - if (missiles[missileIndex].IsAssignable()) { - assignableInterceptorIndices.Add(missileIndex); - } - } - return assignableInterceptorIndices; + [Pure] + protected static List GetAssignableInterceptors(in IReadOnlyList interceptors) { + return interceptors.Where(interceptor => interceptor.IsAssignable()).ToList(); } - // Get the list of active target indices. - protected static List GetActiveThreatIndices(List threats) { - List activeThreatIndices = new List(); - for (int threatIndex = 0; threatIndex < threats.Count; threatIndex++) { - if (!threats[threatIndex].IsHit()) { - activeThreatIndices.Add(threatIndex); - } - } - return activeThreatIndices; + // Get the list of active threats. + [Pure] + protected static List GetActiveThreats(in IReadOnlyList threats) { + return threats.Where(t => t.Status != ThreatStatus.DESTROYED).ToList(); } } diff --git a/Assets/Scripts/Assignment/RoundRobinAssignment.cs b/Assets/Scripts/Assignment/RoundRobinAssignment.cs index f046772..2a35d39 100644 --- a/Assets/Scripts/Assignment/RoundRobinAssignment.cs +++ b/Assets/Scripts/Assignment/RoundRobinAssignment.cs @@ -1,39 +1,43 @@ -using System; using System.Collections.Generic; using System.Linq; using UnityEngine; - -// The round-robin assignment class assigns missiles to the targets in a -// round-robin order. +using System.Diagnostics.Contracts; +// The round-robin assignment class assigns interceptors to the targets in a +// round-robin order using the new paradigm. public class RoundRobinAssignment : IAssignment { - // Previous target index that was assigned. - private int prevTargetIndex = -1; + // Previous target index that was assigned. + private int prevTargetIndex = -1; - // Assign a target to each interceptor that has not been assigned a target yet. - public IEnumerable Assign(List missiles, List targets) { - List assignments = new List(); - List assignableInterceptorIndices = IAssignment.GetAssignableInterceptorIndices(missiles); - if (assignableInterceptorIndices.Count == 0) { - return assignments; + // Assign a target to each interceptor that has not been assigned a target yet. + [Pure] + public IEnumerable Assign(in IReadOnlyList interceptors, in IReadOnlyList targets) { + List assignments = new List(); + + // Get the list of interceptors that are available for assignment. + List assignableInterceptors = IAssignment.GetAssignableInterceptors(interceptors); + if (assignableInterceptors.Count == 0) { + return assignments; + } + + // Get the list of active threats that need to be addressed. + List activeThreats = IAssignment.GetActiveThreats(targets); + if (activeThreats.Count == 0) { + return assignments; + } + + // Perform round-robin assignment. + foreach (Interceptor interceptor in assignableInterceptors) { + // Determine the next target index in a round-robin fashion. + int nextTargetIndex = (prevTargetIndex + 1) % activeThreats.Count; + ThreatData selectedThreat = activeThreats[nextTargetIndex]; + + // Assign the interceptor to the selected threat. + assignments.Add(new IAssignment.AssignmentItem(interceptor, selectedThreat.Threat)); + + // Update the previous target index. + prevTargetIndex = nextTargetIndex; + } + + return assignments; } - - List activeThreatIndices = IAssignment.GetActiveThreatIndices(targets); - if (activeThreatIndices.Count == 0) { - return assignments; - } - - foreach (int missileIndex in assignableInterceptorIndices) { - int nextActiveTargetIndex = activeThreatIndices.FindIndex(index => index > prevTargetIndex); - - if (nextActiveTargetIndex == -1) { - nextActiveTargetIndex = 0; - } - - int nextTargetIndex = activeThreatIndices[nextActiveTargetIndex]; - assignments.Add(new IAssignment.AssignmentItem(missileIndex, nextTargetIndex)); - prevTargetIndex = nextTargetIndex; - } - - return assignments; - } } \ No newline at end of file diff --git a/Assets/Scripts/Assignment/ThreatAssignment.cs b/Assets/Scripts/Assignment/ThreatAssignment.cs index 9b710ff..f0d16dd 100644 --- a/Assets/Scripts/Assignment/ThreatAssignment.cs +++ b/Assets/Scripts/Assignment/ThreatAssignment.cs @@ -3,70 +3,64 @@ using System.Collections.Generic; using System.Linq; using Unity.VisualScripting; using UnityEngine; - -// The threat assignment class assigns missiles to the targets based +using System.Diagnostics.Contracts; +// The threat assignment class assigns interceptors to the targets based // on the threat level of the targets. public class ThreatAssignment : IAssignment { // Assign a target to each interceptor that has not been assigned a target yet. - public IEnumerable Assign(List missiles, List targets) { + [Pure] + public IEnumerable Assign(in IReadOnlyList interceptors, in IReadOnlyList targets) { List assignments = new List(); - List assignableInterceptorIndices = IAssignment.GetAssignableInterceptorIndices(missiles); - if (assignableInterceptorIndices.Count == 0) { + List assignableInterceptors = IAssignment.GetAssignableInterceptors(interceptors); + if (assignableInterceptors.Count == 0) { + Debug.LogWarning("No assignable interceptors found"); return assignments; } - List activeThreatIndices = IAssignment.GetActiveThreatIndices(targets); - if (activeThreatIndices.Count == 0) { + List activeThreats = IAssignment.GetActiveThreats(targets); + if (activeThreats.Count == 0) { + Debug.LogWarning("No active threats found"); return assignments; } Vector3 positionToDefend = Vector3.zero; List threatInfos = - CalculateThreatLevels(targets, activeThreatIndices, positionToDefend); + CalculateThreatLevels(activeThreats, positionToDefend); - foreach (int missileIndex in assignableInterceptorIndices) { - if (missiles[missileIndex].HasAssignedTarget()) - continue; - if (threatInfos.Count == 0) - break; - - // Find the optimal target for this interceptor based on distance and threat - ThreatInfo optimalTarget = null; - float optimalScore = float.MinValue; - - foreach (ThreatInfo threat in threatInfos) { - float distance = Vector3.Distance(missiles[missileIndex].transform.position, - targets[threat.TargetIndex].transform.position); - float score = threat.ThreatLevel / distance; // Balance threat level with proximity - - if (score > optimalScore) { - optimalScore = score; - optimalTarget = threat; + // Sort ThreatInfo first by ThreatData.Status (UNASSIGNED first, then ASSIGNED) + // Within each group, order by ThreatLevel descending + threatInfos = threatInfos.OrderByDescending(t => t.ThreatData.Status == ThreatStatus.UNASSIGNED) + .ThenByDescending(t => t.ThreatLevel) + .ToList(); + + var assignableInterceptorsEnumerator = assignableInterceptors.GetEnumerator(); + if (assignableInterceptorsEnumerator.MoveNext()) // Move to the first element + { + foreach (ThreatInfo threatInfo in threatInfos) { + assignments.Add(new IAssignment.AssignmentItem(assignableInterceptorsEnumerator.Current, threatInfo.ThreatData.Threat)); + if (!assignableInterceptorsEnumerator.MoveNext()) { + break; } } - - if (optimalTarget != null) { - assignments.Add(new IAssignment.AssignmentItem(missileIndex, optimalTarget.TargetIndex)); - threatInfos.Remove(optimalTarget); - } } return assignments; } - private List CalculateThreatLevels(List targets, List activeThreatIndices, - Vector3 missilesMeanPosition) { + + private List CalculateThreatLevels(List threatTable, + Vector3 defensePosition) { List threatInfos = new List(); - foreach (int targetIndex in activeThreatIndices) { - Agent target = targets[targetIndex]; - float distanceToMean = Vector3.Distance(target.transform.position, missilesMeanPosition); - float velocityMagnitude = target.GetVelocity().magnitude; + foreach (ThreatData threatData in threatTable) { + Threat threat = threatData.Threat; + float distanceToMean = Vector3.Distance(threat.transform.position, defensePosition); + float velocityMagnitude = threat.GetVelocity().magnitude; // Calculate threat level based on proximity and velocity float threatLevel = (1 / distanceToMean) * velocityMagnitude; - threatInfos.Add(new ThreatInfo(targetIndex, threatLevel)); + threatInfos.Add(new ThreatInfo(threatData, threatLevel)); } // Sort threats in descending order @@ -74,11 +68,11 @@ public class ThreatAssignment : IAssignment { } private class ThreatInfo { - public int TargetIndex { get; } + public ThreatData ThreatData { get; } public float ThreatLevel { get; } - public ThreatInfo(int targetIndex, float threatLevel) { - TargetIndex = targetIndex; + public ThreatInfo(ThreatData threatData, float threatLevel) { + ThreatData = threatData; ThreatLevel = threatLevel; } } diff --git a/Assets/Scripts/IADS/ThreatData.cs b/Assets/Scripts/IADS/ThreatData.cs new file mode 100644 index 0000000..1cfe9bb --- /dev/null +++ b/Assets/Scripts/IADS/ThreatData.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using UnityEngine; + +[System.Serializable] +public enum ThreatStatus { + UNASSIGNED, + ASSIGNED, + DESTROYED +} +[System.Serializable] +public class ThreatData +{ + public Threat Threat; + [SerializeField] + private ThreatStatus _status; + public ThreatStatus Status { get { return _status; } } + public string ThreatID; + [SerializeField] + private List _assignedInterceptors; // Changed from property to field + + public void AssignInterceptor(Interceptor interceptor) { + if(Status == ThreatStatus.DESTROYED) { + Debug.LogError($"AssignInterceptor: Threat {ThreatID} is destroyed, cannot assign interceptor"); + return; + } + _status = ThreatStatus.ASSIGNED; + _assignedInterceptors.Add(interceptor); + } + + public void RemoveInterceptor(Interceptor interceptor) { + _assignedInterceptors.Remove(interceptor); + if(_assignedInterceptors.Count == 0) { + _status = ThreatStatus.UNASSIGNED; + } + } + + public void MarkDestroyed() { + _status = ThreatStatus.DESTROYED; + } + // Constructor remains the same + public ThreatData(Threat threat, string threatID) + { + Threat = threat; + _status = ThreatStatus.UNASSIGNED; + ThreatID = threatID; + _assignedInterceptors = new List(); // Initialize the list + } +} \ No newline at end of file diff --git a/Assets/Scripts/IADS/ThreatData.cs.meta b/Assets/Scripts/IADS/ThreatData.cs.meta new file mode 100644 index 0000000..f642190 --- /dev/null +++ b/Assets/Scripts/IADS/ThreatData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: adc0c5dbdb9dc7d498b50cf9a15c2db5 \ No newline at end of file diff --git a/Assets/Scripts/Interceptor.cs b/Assets/Scripts/Interceptor.cs index 71ba687..7fa2d9d 100644 --- a/Assets/Scripts/Interceptor.cs +++ b/Assets/Scripts/Interceptor.cs @@ -74,7 +74,7 @@ public class Interceptor : Agent { private void OnTriggerEnter(Collider other) { if (other.gameObject.name == "Floor") { - this.MarkAsMiss(); + this.HandleInterceptMiss(); } // Check if the collision is with another Agent Agent otherAgent = other.gameObject.GetComponentInParent(); @@ -87,13 +87,13 @@ public class Interceptor : Agent { // Set green for hit markerObject.GetComponent().material.color = new Color(0, 1, 0, 0.15f); // Mark both this agent and the other agent as hit - this.MarkAsHit(); - otherAgent.MarkAsHit(); + this.HandleInterceptHit(); + otherAgent.HandleInterceptHit(); } else { // Set red for miss markerObject.GetComponent().material.color = new Color(1, 0, 0, 0.15f); - this.MarkAsMiss(); + this.HandleInterceptMiss(); // otherAgent.MarkAsMiss(); } } diff --git a/Assets/Scripts/Interceptors/Micromissile.cs b/Assets/Scripts/Interceptors/Micromissile.cs index ad9244b..934a8e7 100644 --- a/Assets/Scripts/Interceptors/Micromissile.cs +++ b/Assets/Scripts/Interceptors/Micromissile.cs @@ -28,7 +28,7 @@ public class Micromissile : Interceptor { // Check whether the threat should be considered a miss SensorOutput sensorOutput = GetComponent().Sense(_target); if (sensorOutput.velocity.range > 1000f) { - this.MarkAsMiss(); + this.HandleInterceptMiss(); } // Calculate the acceleration input diff --git a/Assets/Scripts/MicromissileAssembly.asmdef b/Assets/Scripts/MicromissileAssembly.asmdef new file mode 100644 index 0000000..0465970 --- /dev/null +++ b/Assets/Scripts/MicromissileAssembly.asmdef @@ -0,0 +1,16 @@ +{ + "name": "MicromissileAssembly", + "rootNamespace": "", + "references": [ + "GUID:6055be8ebefd69e48b49212b09b47b2f" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Scripts/MicromissileAssembly.asmdef.meta b/Assets/Scripts/MicromissileAssembly.asmdef.meta new file mode 100644 index 0000000..4b4457a --- /dev/null +++ b/Assets/Scripts/MicromissileAssembly.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: af8bba08a36038347823e2f46bdc9857 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Monitor.cs b/Assets/Scripts/Monitor.cs index 1bb7c18..e4fda2a 100644 --- a/Assets/Scripts/Monitor.cs +++ b/Assets/Scripts/Monitor.cs @@ -41,7 +41,7 @@ public class SimMonitor : MonoBehaviour private void ExportTelemetry() { float time = (float)SimManager.Instance.GetElapsedSimulationTime(); - foreach (var agent in SimManager.Instance.GetActiveThreats().Cast().Concat(SimManager.Instance.GetActiveInterceptors().Cast())) + foreach (var agent in SimManager.Instance.GetActiveAgents()) { Vector3 pos = agent.transform.position; if(pos == Vector3.zero) { diff --git a/Assets/Scripts/SimManager.cs b/Assets/Scripts/SimManager.cs index da97c7f..4d2d6a8 100644 --- a/Assets/Scripts/SimManager.cs +++ b/Assets/Scripts/SimManager.cs @@ -19,11 +19,15 @@ public class SimManager : MonoBehaviour { [SerializeField] public SimulationConfig simulationConfig; - private List _interceptors = new List(); + private List _activeInterceptors = new List(); - private List _unassignedThreats = new List(); - private List _threats = new List(); - private List _activeThreats = new List(); + [SerializeField] + private List _threatTable = new List(); + private Dictionary _threatDataMap = new Dictionary(); + + private List _interceptorObjects = new List(); + private List _threatObjects = new List(); + private float _elapsedSimulationTime = 0f; private float endTime = 100f; // Set an appropriate end time private bool simulationRunning = false; @@ -47,12 +51,12 @@ public class SimManager : MonoBehaviour { } public List GetActiveThreats() { - return _activeThreats; + return _threatTable.Where(threat => threat.Status != ThreatStatus.DESTROYED).Select(threat => threat.Threat).ToList(); } public List GetActiveAgents() { return _activeInterceptors.ConvertAll(interceptor => interceptor as Agent) - .Concat(_activeThreats.ConvertAll(threat => threat as Agent)) + .Concat(GetActiveThreats().ConvertAll(threat => threat as Agent)) .ToList(); } @@ -108,8 +112,6 @@ public class SimManager : MonoBehaviour { foreach (var swarmConfig in simulationConfig.interceptor_swarm_configs) { for (int i = 0; i < swarmConfig.num_agents; i++) { var interceptor = CreateInterceptor(swarmConfig.agent_config); - interceptor.OnAgentHit += RegisterInterceptorHit; - interceptor.OnAgentMiss += RegisterInterceptorMiss; } } @@ -118,8 +120,6 @@ public class SimManager : MonoBehaviour { foreach (var swarmConfig in simulationConfig.threat_swarm_configs) { for (int i = 0; i < swarmConfig.num_agents; i++) { var threat = CreateThreat(swarmConfig.agent_config); - threat.OnAgentHit += RegisterThreatHit; - threat.OnAgentMiss += RegisterThreatMiss; } } @@ -131,31 +131,35 @@ public class SimManager : MonoBehaviour { } public void AssignInterceptorsToThreats() { - AssignInterceptorsToThreats(_interceptors); + AssignInterceptorsToThreats(_interceptorObjects); } - public void RegisterInterceptorHit(Agent interceptor) { + public void RegisterInterceptorHit(Interceptor interceptor, Threat threat) { if (interceptor is Interceptor missileComponent) { _activeInterceptors.Remove(missileComponent); } } - public void RegisterInterceptorMiss(Agent interceptor) { + public void RegisterInterceptorMiss(Interceptor interceptor, Threat threat) { if (interceptor is Interceptor missileComponent) { _activeInterceptors.Remove(missileComponent); } + // Remove the interceptor from the threat's assigned interceptors + _threatDataMap[threat].RemoveInterceptor(interceptor); } - public void RegisterThreatHit(Agent threat) { - if (threat is Threat targetComponent) { - _activeThreats.Remove(targetComponent); + public void RegisterThreatHit(Interceptor interceptor, Threat threat) { + ThreatData threatData = _threatDataMap[threat]; + threatData.RemoveInterceptor(interceptor); + if (threatData != null) { + threatData.MarkDestroyed(); } } - public void RegisterThreatMiss(Agent threat) { - if (threat is Threat targetComponent) { - _unassignedThreats.Add(targetComponent); - } + public void RegisterThreatMiss(Interceptor interceptor, Threat threat) { + Debug.Log($"RegisterThreatMiss: Interceptor {interceptor.name} missed threat {threat.name}"); + ThreatData threatData = _threatDataMap[threat]; + threatData.RemoveInterceptor(interceptor); } /// @@ -163,27 +167,34 @@ public class SimManager : MonoBehaviour { /// /// The list of missiles to assign. public void AssignInterceptorsToThreats(List missilesToAssign) { - // Convert Interceptor and Threat lists to Agent lists - List missileAgents = new List(missilesToAssign.ConvertAll(m => m as Agent)); - // Convert Threat list to Agent list, excluding already assigned targets - List targetAgents = _unassignedThreats.ToList(); - // Perform the assignment IEnumerable assignments = - _assignmentScheme.Assign(missileAgents, targetAgents); + _assignmentScheme.Assign(missilesToAssign, _threatTable); // Apply the assignments to the missiles foreach (var assignment in assignments) { - if (assignment.InterceptorIndex < missilesToAssign.Count) { - Interceptor interceptor = missilesToAssign[assignment.InterceptorIndex]; - Threat threat = _unassignedThreats[assignment.ThreatIndex]; - interceptor.AssignTarget(threat); - Debug.Log($"Interceptor {interceptor.name} assigned to threat {threat.name}"); - } + Debug.LogWarning($"Assigning interceptor {assignment.Interceptor} to threat {assignment.Threat}"); + assignment.Interceptor.AssignTarget(assignment.Threat); + _threatDataMap[assignment.Threat].AssignInterceptor(assignment.Interceptor); + Debug.Log($"Interceptor {assignment.Interceptor.name} assigned to threat {assignment.Threat.name}"); + } + + // Check if any interceptors were not assigned + List unassignedInterceptors = missilesToAssign.Where(m => !m.HasAssignedTarget()).ToList(); + + if (unassignedInterceptors.Count > 0) + { + string unassignedIds = string.Join(", ", unassignedInterceptors.Select(m => m.name)); + int totalInterceptors = missilesToAssign.Count; + int assignedInterceptors = totalInterceptors - unassignedInterceptors.Count; + + Debug.LogWarning($"Warning: {unassignedInterceptors.Count} out of {totalInterceptors} interceptors were not assigned to any threat. " + + $"Unassigned interceptor IDs: {unassignedIds}. " + + $"Total interceptors: {totalInterceptors}, Assigned: {assignedInterceptors}, Unassigned: {unassignedInterceptors.Count}"); + + // Log information about the assignment scheme + Debug.Log($"Current Assignment Scheme: {_assignmentScheme.GetType().Name}"); } - // TODO this whole function should be optimized - _unassignedThreats.RemoveAll( - threat => missilesToAssign.Any(interceptor => interceptor.GetAssignedTarget() == threat)); } /// @@ -196,27 +207,32 @@ public class SimManager : MonoBehaviour { InterceptorType.MICROMISSILE => "Micromissile", _ => "Hydra70" }; - GameObject missileObject = CreateAgent(config, prefabName); - if (missileObject == null) + GameObject interceptorObject = CreateAgent(config, prefabName); + if (interceptorObject == null) return null; // Interceptor-specific logic switch (config.dynamic_config.sensor_config.type) { case SensorType.IDEAL: - missileObject.AddComponent(); + interceptorObject.AddComponent(); break; default: Debug.LogError($"Sensor type '{config.dynamic_config.sensor_config.type}' not found."); break; } - _interceptors.Add(missileObject.GetComponent()); - _activeInterceptors.Add(missileObject.GetComponent()); + Interceptor interceptor = interceptorObject.GetComponent(); + _interceptorObjects.Add(interceptor); + _activeInterceptors.Add(interceptor); + + // Subscribe events + interceptor.OnInterceptHit += RegisterInterceptorHit; + interceptor.OnInterceptMiss += RegisterInterceptorMiss; // Assign a unique and simple ID - int missileId = _interceptors.Count; - missileObject.name = $"{config.interceptor_type}_Interceptor_{missileId}"; - return missileObject.GetComponent(); + int missileId = _interceptorObjects.Count; + interceptorObject.name = $"{config.interceptor_type}_Interceptor_{missileId}"; + return interceptorObject.GetComponent(); } /// @@ -233,13 +249,20 @@ public class SimManager : MonoBehaviour { if (threatObject == null) return null; - _threats.Add(threatObject.GetComponent()); - _activeThreats.Add(threatObject.GetComponent()); - _unassignedThreats.Add(threatObject.GetComponent()); - + Threat threat = threatObject.GetComponent(); // Assign a unique and simple ID - int targetId = _threats.Count; + int targetId = _threatTable.Count; threatObject.name = $"{config.threat_type}_Target_{targetId}"; + + ThreatData threatData = new ThreatData(threat, threatObject.name); + _threatDataMap.Add(threat, threatData); + _threatTable.Add(threatData); + _threatObjects.Add(threat); + + // Subscribe events + threat.OnInterceptHit += RegisterThreatHit; + threat.OnInterceptMiss += RegisterThreatMiss; + return threatObject.GetComponent(); } @@ -290,21 +313,24 @@ public class SimManager : MonoBehaviour { simulationRunning = IsSimulationRunning(); // Clear existing missiles and targets - foreach (var interceptor in _interceptors) { + foreach (var interceptor in _interceptorObjects) { if (interceptor != null) { Destroy(interceptor.gameObject); } } - foreach (var threat in _threats) { + foreach (var threat in _threatObjects) { if (threat != null) { Destroy(threat.gameObject); } } - _interceptors.Clear(); - _threats.Clear(); - _unassignedThreats.Clear(); + _interceptorObjects.Clear(); + _activeInterceptors.Clear(); + _threatObjects.Clear(); + _threatTable.Clear(); + + StartSimulation(); } @@ -312,7 +338,7 @@ public class SimManager : MonoBehaviour { void Update() { // Check if all missiles have terminated bool allInterceptorsTerminated = true; - foreach (var interceptor in _interceptors) { + foreach (var interceptor in _interceptorObjects) { if (interceptor != null && !interceptor.IsHit() && !interceptor.IsMiss()) { allInterceptorsTerminated = false; break; diff --git a/Assets/ss.meta b/Assets/Tests.meta similarity index 77% rename from Assets/ss.meta rename to Assets/Tests.meta index 3682cae..e1c70d3 100644 --- a/Assets/ss.meta +++ b/Assets/Tests.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: f034d27b4aab67a47865af3d624c4375 +guid: 5ab47dc725c65cb4cb1fa6948fb0c9a7 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Tests/SanityTest.cs b/Assets/Tests/SanityTest.cs new file mode 100644 index 0000000..8bbd62b --- /dev/null +++ b/Assets/Tests/SanityTest.cs @@ -0,0 +1,25 @@ +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using System.Collections; + +public class SanityTest +{ + [Test] + public void SanityTestSimplePasses() + { + // Use the Assert class to test conditions + Assert.Pass("This test passes."); + } + + // A UnityTest behaves like a coroutine in Play Mode. In Edit Mode you can use + // `yield return null;` to skip a frame. + [UnityTest] + public IEnumerator SanityTestWithEnumeratorPasses() + { + // Use the Assert class to test conditions. + // Use yield to skip a frame. + yield return null; + Assert.Pass("This test passes after skipping a frame."); + } +} \ No newline at end of file diff --git a/Assets/Tests/SanityTest.cs.meta b/Assets/Tests/SanityTest.cs.meta new file mode 100644 index 0000000..bfd1db6 --- /dev/null +++ b/Assets/Tests/SanityTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b48cf6e434723a449a49186eeda32d3f \ No newline at end of file diff --git a/Assets/Tests/Tests.asmdef b/Assets/Tests/Tests.asmdef new file mode 100644 index 0000000..2e95de2 --- /dev/null +++ b/Assets/Tests/Tests.asmdef @@ -0,0 +1,24 @@ +{ + "name": "Tests", + "rootNamespace": "", + "references": [ + "UnityEngine.TestRunner", + "UnityEditor.TestRunner", + "MicromissileAssembly" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Tests/Tests.asmdef.meta b/Assets/Tests/Tests.asmdef.meta new file mode 100644 index 0000000..25221cb --- /dev/null +++ b/Assets/Tests/Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d15c92e585e721749b63d85007276dbe +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/ThreatAssignmentTests.cs b/Assets/Tests/ThreatAssignmentTests.cs new file mode 100644 index 0000000..b7aa303 --- /dev/null +++ b/Assets/Tests/ThreatAssignmentTests.cs @@ -0,0 +1,138 @@ +using NUnit.Framework; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.TestTools; +using System.Linq; + +public class ThreatAssignmentTests +{ + [Test] + public void Assign_Should_Assign_All_Interceptors_And_Threats() + { + // Arrange + ThreatAssignment threatAssignment = new ThreatAssignment(); + + // Create interceptors + List interceptors = new List + { + new GameObject("Interceptor 1").AddComponent(), + new GameObject("Interceptor 2").AddComponent(), + new GameObject("Interceptor 3").AddComponent() + }; + + // Create threats + Threat threat1 = new GameObject("Threat 1").AddComponent(); + Threat threat2 = new GameObject("Threat 2").AddComponent(); + Threat threat3 = new GameObject("Threat 3").AddComponent(); + + // Add Rigidbody components to threats to set velocities + Rigidbody rb1 = threat1.gameObject.AddComponent(); + Rigidbody rb2 = threat2.gameObject.AddComponent(); + Rigidbody rb3 = threat3.gameObject.AddComponent(); + + // Set positions and velocities + threat1.transform.position = Vector3.forward * -20f; + threat2.transform.position = Vector3.forward * -20f; + threat3.transform.position = Vector3.forward * -20f; + + rb1.linearVelocity = Vector3.forward * 5f; + rb2.linearVelocity = Vector3.forward * 10f; + rb3.linearVelocity = Vector3.forward * 15f; + + // Create threat data + List threats = new List + { + new ThreatData(threat1, "Threat1ID"), + new ThreatData(threat2, "Threat2ID"), + new ThreatData(threat3, "Threat3ID") + }; + + // Act + IEnumerable assignments = threatAssignment.Assign(interceptors, threats); + + // Assert + Assert.AreEqual(3, assignments.Count(), "All interceptors should be assigned"); + + HashSet assignedInterceptors = new HashSet(); + HashSet assignedThreats = new HashSet(); + + foreach (var assignment in assignments) + { + Assert.IsNotNull(assignment.Interceptor, "Interceptor should not be null"); + Assert.IsNotNull(assignment.Threat, "Threat should not be null"); + assignedInterceptors.Add(assignment.Interceptor); + assignedThreats.Add(assignment.Threat); + } + + Assert.AreEqual(3, assignedInterceptors.Count, "All interceptors should be unique"); + Assert.AreEqual(3, assignedThreats.Count, "All threats should be assigned"); + + // Verify that threats are assigned in order of their threat level (based on velocity and distance) + var orderedAssignments = assignments.OrderByDescending(a => a.Threat.GetVelocity().magnitude / Vector3.Distance(a.Threat.transform.position, Vector3.zero)).ToList(); + Assert.AreEqual(threat3, orderedAssignments[0].Threat, "Highest threat should be assigned first"); + Assert.AreEqual(threat2, orderedAssignments[1].Threat, "Second highest threat should be assigned second"); + Assert.AreEqual(threat1, orderedAssignments[2].Threat, "Lowest threat should be assigned last"); + } + + [Test] + public void Assign_Should_Handle_More_Interceptors_Than_Threats() + { + // Arrange + ThreatAssignment threatAssignment = new ThreatAssignment(); + + // Create interceptors + List interceptors = new List + { + new GameObject("Interceptor 1").AddComponent(), + new GameObject("Interceptor 2").AddComponent(), + new GameObject("Interceptor 3").AddComponent() + }; + + // Create threats + Threat threat1 = new GameObject("Threat 1").AddComponent(); + Threat threat2 = new GameObject("Threat 2").AddComponent(); + + // Add Rigidbody components to threats to set velocities + Rigidbody rb1 = threat1.gameObject.AddComponent(); + Rigidbody rb2 = threat2.gameObject.AddComponent(); + + // Set positions and velocities + threat1.transform.position = Vector3.up * 10f; + threat2.transform.position = Vector3.right * 5f; + + rb1.linearVelocity = Vector3.forward * 10f; + rb2.linearVelocity = Vector3.forward * 15f; + + // Create threat data + List threats = new List + { + new ThreatData(threat1, "Threat1ID"), + new ThreatData(threat2, "Threat2ID") + }; + + // Act + IEnumerable assignments = threatAssignment.Assign(interceptors, threats); + + // Assert + Assert.AreEqual(2, assignments.Count(), "All threats should be assigned"); + + HashSet assignedInterceptors = new HashSet(); + HashSet assignedThreats = new HashSet(); + + foreach (var assignment in assignments) + { + Assert.IsNotNull(assignment.Interceptor, "Interceptor should not be null"); + Assert.IsNotNull(assignment.Threat, "Threat should not be null"); + assignedInterceptors.Add(assignment.Interceptor); + assignedThreats.Add(assignment.Threat); + } + + Assert.AreEqual(2, assignedInterceptors.Count, "Two interceptors should be assigned"); + Assert.AreEqual(2, assignedThreats.Count, "Both threats should be assigned"); + + // Verify that threats are assigned in order of their threat level (based on velocity and distance) + var orderedAssignments = assignments.OrderByDescending(a => a.Threat.GetVelocity().magnitude / Vector3.Distance(a.Threat.transform.position, Vector3.zero)).ToList(); + Assert.AreEqual(threat2, orderedAssignments[0].Threat, "Higher threat should be assigned first"); + Assert.AreEqual(threat1, orderedAssignments[1].Threat, "Lower threat should be assigned second"); + } +} \ No newline at end of file diff --git a/Assets/Tests/ThreatAssignmentTests.cs.meta b/Assets/Tests/ThreatAssignmentTests.cs.meta new file mode 100644 index 0000000..687ccb1 --- /dev/null +++ b/Assets/Tests/ThreatAssignmentTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7ddf271569a78ee4e995192d4df0ef3f \ No newline at end of file diff --git a/ProjectSettings/SceneTemplateSettings.json b/ProjectSettings/SceneTemplateSettings.json index 5e97f83..1edced2 100644 --- a/ProjectSettings/SceneTemplateSettings.json +++ b/ProjectSettings/SceneTemplateSettings.json @@ -61,6 +61,11 @@ "type": "UnityEngine.PhysicMaterial", "defaultInstantiationMode": 0 }, + { + "userAdded": false, + "type": "UnityEngine.PhysicsMaterial", + "defaultInstantiationMode": 0 + }, { "userAdded": false, "type": "UnityEngine.PhysicsMaterial2D",