using System; using System.Collections.Generic; namespace ProjectI.Enemies { public enum EnemyRecoveryAssessment { Infeasible, Useful, Unknown, Deferred } // Completed feasibility evidence is separate from graph results and pursuit expiry. public sealed class EnemyRecoveryPolicy { private const int Alternatives = 6; private readonly Dictionary completed = new Dictionary(4); private readonly double observationSeconds; private ulong revision; private double previousAt; private bool wasEligible, active; public int CompletedAlternatives { get; private set; } public double EligibleSeconds { get; private set; } public bool CanRetire => active && revision != 0 && wasEligible && completed.Count > 0 && CompletedAlternatives == Alternatives * completed.Count && EligibleSeconds >= observationSeconds; public EnemyRecoveryPolicy(double observationSeconds = 8) { if (!Finite(observationSeconds) || observationSeconds < 8 || observationSeconds > 120) throw new ArgumentOutOfRangeException(nameof(observationSeconds)); this.observationSeconds = observationSeconds; } public void Begin(ulong opportunityRevision, IEnumerable targets, double now) { if (opportunityRevision == 0 || !Finite(now) || now < 0 || targets == null) throw new ArgumentException("Invalid recovery scope."); var unique = new HashSet(); foreach (var target in targets) if (target.SpawnLifetime == 0 || !unique.Add(target) || unique.Count > 4) throw new ArgumentException("Recovery needs distinct living target lifetimes."); if (unique.Count == 0) throw new ArgumentException("Zero living targets belongs to run lifecycle."); if (opportunityRevision == revision) { if (!unique.SetEquals(completed.Keys)) throw new ArgumentException("A changed target set needs a new opportunity."); return; } if (opportunityRevision < revision) throw new ArgumentException("Recovery opportunity cannot rewind."); revision = opportunityRevision; active = true; completed.Clear(); foreach (var target in unique) completed.Add(target, 0); CompletedAlternatives = 0; EligibleSeconds = 0; previousAt = now; wasEligible = false; } public void Advance(double now, bool eligible) { if (!active || revision == 0 || !Finite(now) || now < previousAt) { wasEligible = false; return; } double elapsed = now - previousAt; // No offline/stalled interval or temporarily unavailable endpoint can fund the window. if (eligible && wasEligible && elapsed <= .25) EligibleSeconds += elapsed; previousAt = now; wasEligible = eligible; } public bool RecordCompleted(ulong opportunityRevision, EnemyTargetKey target, int alternative, EnemyRecoveryAssessment result) { if (!active || opportunityRevision != revision || revision == 0 || alternative < 0 || alternative >= Alternatives || !completed.TryGetValue(target, out int mask) || !Enum.IsDefined(typeof(EnemyRecoveryAssessment), result)) return false; if (result == EnemyRecoveryAssessment.Useful) { foreach (var key in new List(completed.Keys)) completed[key] = 0; // A useful contribution ends this evidence generation; late failures cannot reopen it. CompletedAlternatives = 0; EligibleSeconds = 0; wasEligible = false; active = false; return true; } int bit = 1 << alternative; if (result != EnemyRecoveryAssessment.Infeasible) { if (result == EnemyRecoveryAssessment.Unknown && (mask & bit) != 0) { completed[target] = mask & ~bit; CompletedAlternatives--; } wasEligible = false; return false; } if ((mask & bit) != 0) return false; completed[target] = mask | bit; CompletedAlternatives++; return true; } public bool TryGetNext(out EnemyTargetKey target, out int alternative) { if (!active) { target = default; alternative = 0; return false; } foreach (var pair in completed) for (int i = 0; i < Alternatives; i++) if ((pair.Value & (1 << i)) == 0) { target = pair.Key; alternative = i; return true; } target = default; alternative = 0; return false; } private static bool Finite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); } }