Unit test program for the adaptive director and music systems. It runs a large suite of deterministic tests exercising pressure, placement, scheduling, resource and orchestration logic, music authority/runtime, persistence, and determinism across many subsystems.
using SboxDirector;
static class Tests
{
static int _count;
static void Check(bool value, string name) { _count++; if (!value) throw new Exception($"FAILED: {name}"); }
static void Near(float actual, float expected, float epsilon, string name) => Check(MathF.Abs(actual - expected) <= epsilon, $"{name}: expected {expected}, got {actual}");
public static void Main()
{
Pressure(); TeamMaximum(); Tempo(); Placement(); AdvancedPlacement(); RecoveredCompatibility(); Population(); Wave(); Scenarios(); Core(); Persistence(); PersistentPolicies(); RuleSchedules(); ConcurrentSchedules(); CompositionAndRetry(); Resources(); Orchestration(); AdaptiveCallbacks(); HostRunner(); OwnershipAndResources(); MusicAuthority(); MusicRuntimeArbitration(); MusicPersistenceAndHost(); Validation(); LongDeterminism(); AdvancedDeterminism();
Console.WriteLine($"PASS: {_count} adaptive-director assertions");
}
static void Pressure()
{
var p = new PressureTracker(new PressureConfiguration(), 0);
p.Add(PressureSeverity.Major, 0); Near(p.Instantaneous, .125f, .00001f, "major coefficient");
p.Update(5); Near(p.Instantaneous, .125f, .00001f, "hold");
p.Update(8); Near(p.Instantaneous, .025f, .0001f, "linear decay");
p.Add(PressureSeverity.Maximum, 8); Near(p.Instantaneous, 1f, .00001f, "maximum saturates");
var q = new PressureTracker(new PressureConfiguration(), 0); q.Add(PressureSeverity.Critical, 0); q.Update(1f / 15f);
Near(q.Averaged, MathF.Pow(.25f, (1f / 15f) / 20f), .0001f, "recovered averaged formula");
}
static void TeamMaximum()
{
var team = new TeamPressure(new PressureConfiguration()); team.Apply(new("a", PressureSeverity.Critical, "test"), 0); team.Apply(new("b", PressureSeverity.Minor, "test"), 0);
var world = new WorldSnapshot { Time = 0, Participants = new[] { new ParticipantSnapshot { Id = "a" }, new ParticipantSnapshot { Id = "b" } } };
Near(team.UpdateAndGetMaximum(world), .25f, .0001f, "team uses max");
}
static void Tempo()
{
var c = new TempoController(new TempoConfiguration { RelaxMinimumSeconds = 1, RelaxMaximumSeconds = 2, RelaxMaximumProgressTravel = .1f });
c.Update(new WorldSnapshot { Time = 1, TeamProgress = .2f }, 0); Check(c.Phase == TempoPhase.BuildUp, "relax to buildup");
c.Update(new WorldSnapshot { Time = 20 }, 1); Check(c.Phase == TempoPhase.SustainPeak, "buildup to peak");
var absoluteClock = new DirectorCore(new DirectorConfiguration { Tempo = new TempoConfiguration { RelaxMinimumSeconds = 20, RelaxMaximumSeconds = 45 } }); Check(absoluteClock.Tick(World(10000)).Tempo == TempoPhase.Relax, "first tick aligns absolute clock");
}
static void Placement()
{
var candidates = new[] { new SpawnCandidate { Id = "visible", Reachable = true, Spawnable = true, Visible = true, NearestParticipantDistance = 1000, ThreatSeparation = 1000 }, new SpawnCandidate { Id = "ok", Reachable = true, Spawnable = true, NearestParticipantDistance = 1200, ThreatSeparation = 1000 } };
var selected = new PlacementSelector().Select(candidates, new PlacementProfile(), new DeterministicRandom(1)); Check(selected?.Candidate.Id == "ok", "placement filters visibility");
var vetoed = new PlacementSelector().Select(candidates, new PlacementProfile(), new DeterministicRandom(1), new WorldSnapshot(), new RejectAllPlacement()); Check(vetoed is null, "custom placement veto");
var tagged = new SpawnCandidate { Id = "tagged", Reachable = true, Spawnable = true, NearestParticipantDistance = 1200, ThreatSeparation = 1000, Tags = new HashSet<string> { "outdoor" } }; Check(new PlacementSelector().Select(new[] { tagged }, new PlacementProfile { RequiredTags = new HashSet<string> { "indoor" } }, new DeterministicRandom(1)) is null, "required placement tags");
}
static void AdvancedPlacement()
{
var strict = new SpawnCandidate { Id = "strict", Reachable = true, Spawnable = true, NearestParticipantDistance = 1200, ThreatSeparation = 2000, PathDistance = 800, IngressDistance = 300, ClearRadius = 500, AdvanceRelation = AdvanceRelation.Ahead };
var fallback = new SpawnCandidate { Id = "fallback", Reachable = true, Spawnable = true, NearestParticipantDistance = 1200, ThreatSeparation = 2000, PathDistance = 20, IngressDistance = 10, ClearRadius = 20, AdvanceRelation = AdvanceRelation.Behind };
var profile = new PlacementProfile { MinimumPathDistance = 500, MinimumIngressDistance = 200, MinimumClearRadius = 300, AllowedRelations = new HashSet<AdvanceRelation> { AdvanceRelation.Ahead }, FallbackMode = PlacementFallbackMode.IgnoreRouteConstraints };
var selected = new PlacementSelector().Select(new[] { strict, fallback }, profile, new DeterministicRandom(1)); Check(selected?.Candidate.Id == "strict" && !selected.IsFallback, "route-constrained placement");
selected = new PlacementSelector().Select(new[] { fallback }, profile, new DeterministicRandom(1)); Check(selected?.Candidate.Id == "fallback" && selected.IsFallback, "route fallback placement");
}
static void RecoveredCompatibility()
{
var route = new[]
{
new SpawnCandidate { Id = "first-tie", Reachable = true, Spawnable = true, Progress = 50 },
new SpawnCandidate { Id = "lower", Reachable = true, Spawnable = true, Progress = 40 },
new SpawnCandidate { Id = "second-tie", Reachable = true, Spawnable = true, Progress = 50 }
};
var routeChoice = new PlacementSelector().Select(route, RecoveredDirectorDefaults.CreateRouteStepProfile(), new DeterministicRandom(1));
Check(routeChoice?.Candidate.Id == "first-tie", "recovered route uses strict greater-than and first tie");
var threat = new[]
{
new SpawnCandidate { Id = "too-small", Reachable = true, Spawnable = true, AreaWidth = 23, AreaHeight = 24 },
new SpawnCandidate { Id = "a", Reachable = true, Spawnable = true, AreaWidth = 24, AreaHeight = 24 },
new SpawnCandidate { Id = "b", Reachable = true, Spawnable = true, AreaWidth = 30, AreaHeight = 30 }
};
var expectedOrder = threat.ToArray(); var expectedRandom = new DeterministicRandom(17);
for (var index = 0; index < expectedOrder.Length; index++) { var other = expectedRandom.NextInt(expectedOrder.Length); (expectedOrder[index], expectedOrder[other]) = (expectedOrder[other], expectedOrder[index]); }
var expected = expectedOrder.First(x => x.AreaWidth >= 24 && x.AreaHeight >= 24).Id;
var threatChoice = new PlacementSelector().Select(threat, RecoveredDirectorDefaults.CreateThreatAreaProfile(), new DeterministicRandom(17));
Check(threatChoice?.Candidate.Id == expected, "recovered threat shuffle uses full-range random transpositions then first valid");
var samples = HullVisibilitySamples.CreateFivePointPattern(new DirectorVector(10, 0, 2), new DirectorVector(0, 0, 2), 32, 72);
Check(samples.Count == 5, "recovered visibility sample count");
Near(samples[0].Y, 16, .0001f, "lower left hull offset"); Near(samples[1].Y, -16, .0001f, "lower right hull offset");
Near(samples[2].Z, 38, .0001f, "upper center half-height"); Near(samples[3].Z, 74, .0001f, "upper corner full-height");
var uniform = new UniformEncounterComposer(new Dictionary<EncounterKind, IReadOnlyList<string>> { [EncounterKind.Special] = new[] { "a", "b", "c" } });
var composition = uniform.Compose(EncounterKind.Special, 20, new WorldSnapshot(), new DefaultDirectorPolicy(), new DeterministicRandom(3));
Check(composition.Sum(x => x.Count) == 20 && composition.All(x => x.Archetype is "a" or "b" or "c"), "uniform recovered composition");
var rotation = new SpecialClassRotation(new[] { "a", "b", "c" });
rotation.RestoreState(new Dictionary<string, double> { ["a"] = 0, ["b"] = 0, ["c"] = 0 });
var selected = rotation.SelectDue(10, x => x != "a", new DeterministicRandom(4));
Check(selected is "b" or "c", "special rotation selects uniformly from due eligible classes");
Near((float)rotation.CaptureState()["a"], 30, .0001f, "ineligible class retry delay");
rotation.ReportSpawnFailure(selected!, 10); Near((float)rotation.CaptureState()[selected!], 15, .0001f, "failed class retry delay");
rotation.ReportSpawnSuccess(selected!, 20); Near((float)rotation.CaptureState()[selected!], 1019, .0001f, "successful class hold");
rotation.ReleaseAfterLifecycle(selected!, 30); Near((float)rotation.CaptureState()[selected!], 75, .0001f, "special respawn interval");
}
static void Population()
{
var ledger = new PopulationLedger(new PopulationConfiguration()); var empty = new PopulationSnapshot(); Check(ledger.TryReserve(1, EncounterKind.Boss, 1, empty), "reserve"); Check(ledger.Available(EncounterKind.Boss, empty) == 0, "reservation counts"); ledger.Resolve(new(1, RequestResultKind.Failed, 0)); Check(ledger.Available(EncounterKind.Boss, empty) == 1, "failure releases");
Check(ledger.TryReserve(2, EncounterKind.Boss, 1, empty, 10), "reserve deferred"); ledger.Resolve(new(2, RequestResultKind.Deferred, 0)); Check(ledger.Available(EncounterKind.Boss, empty) == 0, "deferred remains reserved"); Check(ledger.Expire(10).SequenceEqual(new long[] { 2 }), "reservation expires"); Check(ledger.Available(EncounterKind.Boss, empty) == 1, "expiry releases");
}
static void Wave()
{
var wave = new WaveSequenceController(new WaveSequenceConfiguration { InitialDelayMinimum = 0, InitialDelayMaximum = 0, CombatTimeout = 0, PauseMinimum = 0, PauseMaximum = 0 }, new DeterministicRandom(2)); wave.Start(0); wave.Update(0, false); Check(wave.Update(0, false).IssueWave, "wave issued"); Check(wave.Update(0, false).Completed, "wave completed");
var restored = new WaveSequenceController(new WaveSequenceConfiguration(), new DeterministicRandom(2), wave.Snapshot); Check(restored.State == WaveState.Done, "wave restore"); restored.Cancel(); Check(restored.State == WaveState.Inactive, "wave cancel");
}
static void Scenarios()
{
var scenario = new ScenarioController(new[] { new ScenarioStageDefinition("one") }); scenario.Start(0); Check(scenario.TryAdvance(0, false) && scenario.Status == ScenarioStatus.Complete, "scenario complete"); var rounds = new RoundSessionFlow(); rounds.BeginRound("1"); Check(rounds.IsSessionActive, "round adapter"); rounds.EndRound(); Check(!rounds.IsSessionActive, "round end"); Check(new ContinuousSessionFlow().IsSessionActive, "continuous adapter");
var restored = new ScenarioController(new[] { new ScenarioStageDefinition("one") }, scenario.Snapshot); Check(restored.Status == ScenarioStatus.Complete, "scenario restore"); restored.Reset(); Check(restored.Status == ScenarioStatus.Inactive, "scenario reset");
}
static void Core()
{
var core = new DirectorCore(new DirectorConfiguration { Tempo = new TempoConfiguration { RelaxMinimumSeconds = 0, RelaxMaximumSeconds = 0 } });
var output = core.Tick(new WorldSnapshot { Time = 0, Participants = new[] { new ParticipantSnapshot { Id = "p" } }, SpawnCandidates = new[] { new SpawnCandidate { Id = "s", Reachable = true, Spawnable = true, NearestParticipantDistance = 800, ThreatSeparation = 2000 } } });
Check(output.Events.Count == 1, "core emits tempo transition");
var multi = new DirectorCore(new DirectorConfiguration { MaximumRequestsPerTick = 2 }, schedule: new RepeatingSchedule()); var requests = multi.Tick(World(0)); Check(requests.SpawnRequests.Count == 2, "multiple requests per tick");
}
static WorldSnapshot World(double time, string? round = null) => new() { Time = time, RoundId = round, Participants = new[] { new ParticipantSnapshot { Id = "p" } }, SpawnCandidates = new[] { new SpawnCandidate { Id = "s", Reachable = true, Spawnable = true, NearestParticipantDistance = 1000, ThreatSeparation = 3000 } } };
static void Persistence()
{
var config = new DirectorConfiguration { Seed = 42 }; Check(DirectorStateCodec.ToJson(new DirectorCore(config).CaptureState()).Contains("HasTicked"), "pre-tick state serializes"); var first = new DirectorCore(config); first.ApplyPressure(new("p", PressureSeverity.Major, "test"), 0); var initial = first.Tick(World(0)); Check(initial.SpawnRequests.Count == 1, "state setup request");
var json = DirectorStateCodec.ToJson(first.CaptureState()); Check(json.Contains("ConfigurationVersion"), "state JSON");
var restored = new DirectorCore(config, DirectorStateCodec.FromJson(json));
var a = first.Tick(World(4)); var b = restored.Tick(World(4)); Check(a.Tempo == b.Tempo && a.TeamPressure == b.TeamPressure && a.SpawnRequests.SequenceEqual(b.SpawnRequests), "restored core remains deterministic");
var diagnostics = new DirectorTelemetryBuffer(restored, 2); restored.ApplyPressure(new("p", PressureSeverity.Minor, "one"), 4); restored.ApplyPressure(new("p", PressureSeverity.Minor, "two"), 4); restored.ApplyPressure(new("p", PressureSeverity.Minor, "three"), 4); Check(diagnostics.Snapshot().Count == 2, "telemetry bounded"); diagnostics.Clear(); Check(diagnostics.Snapshot().Count == 0, "telemetry clear");
Check(restored.CancelPendingRequests().Count > 0, "cancel pending");
}
static void RuleSchedules()
{
var population = new PopulationLedger(new PopulationConfiguration()); var world = World(0); var random = new DeterministicRandom(4);
var rule = new RuleBasedEncounterSchedule(new[] { new EncounterRule { Id = "boss", Kind = EncounterKind.Boss, Count = 1, Priority = 10, CooldownSeconds = 30, Phases = new HashSet<TempoPhase> { TempoPhase.Relax } } });
Check(rule.SelectEncounter(world, TempoPhase.Relax, 0, population, random) == EncounterKind.Boss, "rule selection"); rule.SelectionFinalized(false); Check(rule.SelectEncounter(world, TempoPhase.Relax, 0, population, random) == EncounterKind.Boss, "failed rule rolls cooldown back"); rule.SelectionFinalized(true); Check(rule.SelectEncounter(world, TempoPhase.Relax, 0, population, random) is null, "rule cooldown committed");
var milestone = new MilestoneEncounterSchedule("boss-half", EncounterKind.Boss, .5f); var halfway = new WorldSnapshot { Time = 0, TeamProgress = .6f, ModeId = "campaign", RoundId = "r1" };
Check(milestone.SelectEncounter(halfway, TempoPhase.Relax, 0, population, random) == EncounterKind.Boss, "milestone selected"); milestone.SelectionFinalized(false); Check(milestone.SelectEncounter(halfway, TempoPhase.Relax, 0, population, random) == EncounterKind.Boss, "milestone retry"); milestone.SelectionFinalized(true); Check(milestone.SelectEncounter(halfway, TempoPhase.Relax, 0, population, random) is null, "milestone one-shot per round");
var state = milestone.CaptureScheduleState(); var copy = new MilestoneEncounterSchedule("boss-half", EncounterKind.Boss, .5f); copy.RestoreScheduleState(state); Check(copy.SelectEncounter(halfway, TempoPhase.Relax, 0, population, random) is null, "milestone persistence");
}
static void PersistentPolicies()
{
var configuration = new DirectorConfiguration();
var policy = new StatefulPolicy("tests.policy");
var placement = new StatefulPlacementPolicy("tests.placement");
var composer = new StatefulComposer("tests.composer");
var original = new DirectorCore(configuration, policy: policy, placementPolicy: placement, composer: composer);
original.Tick(World(0));
Check(policy.CallCount > 0 && placement.CallCount > 0, "custom policy state setup");
var json = DirectorStateCodec.ToJson(original.CaptureState());
var restoredPolicy = new StatefulPolicy("tests.policy");
var restoredPlacement = new StatefulPlacementPolicy("tests.placement");
var restoredComposer = new StatefulComposer("tests.composer");
_ = new DirectorCore(configuration, DirectorStateCodec.FromJson(json), policy: restoredPolicy, placementPolicy: restoredPlacement, composer: restoredComposer);
Check(restoredPolicy.CallCount == policy.CallCount, "custom Director policy restored");
Check(restoredPlacement.CallCount == placement.CallCount, "custom placement policy restored");
Check(restoredComposer.CallCount == composer.CallCount, "custom composer restored");
var threw = false;
try { _ = new DirectorCore(configuration, DirectorStateCodec.FromJson(json), policy: new StatefulPolicy("wrong.policy"), placementPolicy: restoredPlacement, composer: restoredComposer); }
catch (InvalidOperationException) { threw = true; }
Check(threw, "custom policy identity mismatch rejected");
threw = false;
try { _ = new DirectorCore(configuration, DirectorStateCodec.FromJson(json)); }
catch (InvalidOperationException) { threw = true; }
Check(threw, "custom policy state cannot be silently discarded");
}
static void ConcurrentSchedules()
{
var schedule = new ConcurrentEncounterSchedule(new[]
{
new ScheduleLane { Id = "ambient", Priority = 10, Schedule = new BasicEncounterSchedule(100, 100) },
new ScheduleLane { Id = "secondary", Priority = 5, Schedule = new BasicEncounterSchedule(100, 100) }
});
var core = new DirectorCore(new DirectorConfiguration { MaximumRequestsPerTick = 2 }, schedule: schedule);
var output = core.Tick(World(0)); Check(output.SpawnRequests.Count == 2, "independent schedule lanes share a tick");
var state = schedule.CaptureScheduleState(); var copy = new ConcurrentEncounterSchedule(new[] { new ScheduleLane { Id = "ambient", Priority = 10, Schedule = new BasicEncounterSchedule(100, 100) }, new ScheduleLane { Id = "secondary", Priority = 5, Schedule = new BasicEncounterSchedule(100, 100) } }); copy.RestoreScheduleState(state); Check(copy.CaptureScheduleState()["coordinator.served"].Contains("ambient"), "concurrent schedule persistence");
}
static void CompositionAndRetry()
{
var composer = new WeightedEncounterComposer(new Dictionary<EncounterKind, IReadOnlyList<WeightedArchetype>> { [EncounterKind.Ambient] = new[] { new WeightedArchetype("grunt", 1) } });
var configuration = new DirectorConfiguration { Retry = new RetryConfiguration { MaximumAttempts = 3, DelaySeconds = 1 } };
var core = new DirectorCore(configuration, composer: composer); var first = core.Tick(World(0)).SpawnRequests.Single(); Check(first.Composition.Single() == new EncounterMember("grunt", 1) && first.Attempt == 1, "weighted composition");
core.ApplyResult(new(first.RequestId, RequestResultKind.Failed, 0, "blocked")); var saved = DirectorStateCodec.FromJson(DirectorStateCodec.ToJson(core.CaptureState())); Check(saved.RetryQueue.Count == 1, "retry persisted");
var restored = new DirectorCore(configuration, saved, composer: composer); Check(restored.Tick(World(.5)).SpawnRequests.Count == 0, "retry delay"); var retry = restored.Tick(World(1)).SpawnRequests.Single(); Check(retry.Attempt == 2 && retry.Composition.SequenceEqual(first.Composition), "retry preserves attempt and composition");
restored.ApplyResult(new(retry.RequestId, RequestResultKind.PartiallySucceeded, 0, "partial")); Check(restored.CaptureState().RetryQueue.Single().RequestedCount == 1, "partial remainder queued");
}
static void Resources()
{
var rules = new[] { new ResourceRule { Category = "healing", TargetCount = 2, MaximumRequestsPerTick = 2 } }; var resources = new ResourcePopulationController(rules, 4);
var core = new DirectorCore(new DirectorConfiguration(), resources: resources); var baseWorld = World(0); var world = new WorldSnapshot { Time = baseWorld.Time, Participants = baseWorld.Participants, SpawnCandidates = baseWorld.SpawnCandidates, Resources = new ResourceWorldSnapshot { Candidates = new[] { new ResourceCandidate { Id = "r1", Category = "healing", AreaCapacity = 2 }, new ResourceCandidate { Id = "r2", Category = "healing", AreaCapacity = 1 } } } };
var output = core.Tick(world); Check(output.ResourceRequests.Count == 2, "resource density requests"); foreach (var request in output.ResourceRequests) core.ApplyResourceResult(new(request.RequestId, RequestResultKind.Succeeded));
var state = core.CaptureState(); Check(state.Resources?.Consumed.Count == 2, "resource revisit state captured");
var restoredResources = new ResourcePopulationController(rules, 9); _ = new DirectorCore(new DirectorConfiguration(), state, resources: restoredResources); Check(restoredResources.CaptureState().Consumed.Count == 2, "resource state restored");
var threw = false; try { _ = new ResourcePopulationController(new[] { new ResourceRule { Category = "healing", TargetCount = 3 } }, state.Resources!.Value); } catch (InvalidOperationException) { threw = true; }
Check(threw, "resource definition mismatch rejected");
var clusterRules = new[] { new ResourceRule { Category = "ammo", TargetCount = 2, MaximumRequestsPerTick = 2, MaximumPerCluster = 1 } }; var clustered = new ResourcePopulationController(clusterRules); var clusteredWorld = new WorldSnapshot { Resources = new ResourceWorldSnapshot { Candidates = new[] { new ResourceCandidate { Id = "a", Category = "ammo", ClusterId = "room" }, new ResourceCandidate { Id = "b", Category = "ammo", ClusterId = "room" } } } }; var clusterRequest = clustered.Tick(clusteredWorld.Resources, clusteredWorld).Single(); Check(clusterRequest.CandidateId is "a" or "b", "resource cluster limit"); clustered.ApplyResult(new(clusterRequest.RequestId, RequestResultKind.Succeeded));
var clusterCopy = new ResourcePopulationController(clusterRules, clustered.CaptureState()); var changedSnapshot = new ResourceWorldSnapshot { Candidates = new[] { new ResourceCandidate { Id = "replacement", Category = "ammo", ClusterId = "room" } } }; Check(clusterCopy.Tick(changedSnapshot, new WorldSnapshot { Resources = changedSnapshot }).Count == 0, "resource cluster history survives absent candidates");
threw = false; try { _ = new ResourcePopulationController(new[] { new ResourceRule { Category = "healing", TargetCount = 2, MaximumRequestsPerTick = 2, RequiredTags = new HashSet<string> { "locked" } } }, state.Resources!.Value); } catch (InvalidOperationException) { threw = true; }
Check(threw, "resource required-tag mismatch rejected");
}
static void Orchestration()
{
var orchestrationConfiguration = new DirectorOrchestrationConfiguration { ScenarioStages = new[] { new ScenarioStageDefinition("boss", Encounter: EncounterKind.Boss, MusicCue: "boss_intro"), new ScenarioStageDefinition("escape") }, Waves = new WaveSequenceConfiguration { InitialDelayMinimum = 0, InitialDelayMaximum = 0, CombatTimeout = 0, PauseMinimum = 0, PauseMaximum = 0 }, WaveEncounterCount = 3 };
var orchestration = new DirectorOrchestration(orchestrationConfiguration); var core = new DirectorCore(new DirectorConfiguration(), orchestration: orchestration); core.StartScenario(0); var stage = core.Tick(World(0)); Check(stage.SpawnRequests.Single().Kind == EncounterKind.Boss && stage.Events.Any(x => x.Name == "music_cue"), "scenario encounter and music cue"); core.ApplyResult(new(stage.SpawnRequests[0].RequestId, RequestResultKind.Succeeded, 1)); core.Tick(World(1)); Check(orchestration.ScenarioStatus == ScenarioStatus.Running, "scenario advances stages");
var saved = core.CaptureState(); var restoredOrchestration = new DirectorOrchestration(orchestrationConfiguration, saved.Orchestration!.Value); _ = new DirectorCore(new DirectorConfiguration(), saved, orchestration: restoredOrchestration); Check(restoredOrchestration.ScenarioStatus == orchestration.ScenarioStatus, "orchestration persistence");
var threw = false; try { _ = new DirectorOrchestration(new DirectorOrchestrationConfiguration { PersistenceId = "wrong" }, saved.Orchestration.Value); } catch (InvalidOperationException) { threw = true; }
Check(threw, "orchestration definition mismatch rejected");
threw = false; try { _ = new DirectorOrchestration(new DirectorOrchestrationConfiguration { ScenarioStages = new[] { new ScenarioStageDefinition("changed") } }, saved.Orchestration.Value); } catch (InvalidOperationException) { threw = true; }
Check(threw, "orchestration content mismatch rejected");
threw = false; try { _ = new DirectorCore(new DirectorConfiguration(), saved, orchestration: new DirectorOrchestration(orchestrationConfiguration)); } catch (InvalidOperationException) { threw = true; }
Check(threw, "fresh orchestration cannot replace restored state");
var waves = new DirectorOrchestration(orchestrationConfiguration); var waveCore = new DirectorCore(new DirectorConfiguration(), orchestration: waves); waveCore.StartWaveSequence(0); waveCore.Tick(World(0)); var issued = waveCore.Tick(World(0)); Check(issued.SpawnRequests.Single().Kind == EncounterKind.CommonWave && issued.SpawnRequests[0].RequestedCount == 3, "integrated panic wave");
var retryConfiguration = new DirectorConfiguration { Retry = new RetryConfiguration { MaximumAttempts = 2, DelaySeconds = 0 } };
var retryScenario = new DirectorOrchestration(new DirectorOrchestrationConfiguration { ScenarioStages = new[] { new ScenarioStageDefinition("required", Encounter: EncounterKind.Boss) } }); var retryScenarioCore = new DirectorCore(retryConfiguration, orchestration: retryScenario); retryScenarioCore.StartScenario(0); var failedStage = retryScenarioCore.Tick(World(0)).SpawnRequests.Single(); retryScenarioCore.ApplyResult(new(failedStage.RequestId, RequestResultKind.Failed, 0)); var stageRetry = retryScenarioCore.Tick(World(0)).SpawnRequests.Single(); Check(stageRetry.Attempt == 2 && retryScenario.ScenarioStatus == ScenarioStatus.Running, "scenario waits for retried host result"); retryScenarioCore.ApplyResult(new(stageRetry.RequestId, RequestResultKind.Succeeded, 1)); Check(retryScenarioCore.Tick(World(0)).Events.Any(x => x.Name == "scenario_advanced") && retryScenario.ScenarioStatus == ScenarioStatus.Complete, "scenario advances after retry success");
var retryWaves = new DirectorOrchestration(new DirectorOrchestrationConfiguration { Waves = new WaveSequenceConfiguration { InitialDelayMinimum = 0, InitialDelayMaximum = 0, CombatTimeout = 0, PauseMinimum = 0, PauseMaximum = 0 } }); var retryWaveCore = new DirectorCore(retryConfiguration, orchestration: retryWaves); retryWaveCore.StartWaveSequence(0); retryWaveCore.Tick(World(0)); var failedWave = retryWaveCore.Tick(World(0)).SpawnRequests.Single(); retryWaveCore.ApplyResult(new(failedWave.RequestId, RequestResultKind.Failed, 0)); var waveRetry = retryWaveCore.Tick(World(0)).SpawnRequests.Single(); Check(waveRetry.Attempt == 2 && retryWaves.WaveActive, "wave retry bypasses ordinary-schedule suppression"); retryWaveCore.ApplyResult(new(waveRetry.RequestId, RequestResultKind.Succeeded, waveRetry.RequestedCount)); Check(retryWaveCore.Tick(World(0)).Events.Any(x => x.Name == "wave_sequence_complete"), "wave waits for retry success before completion");
var terminalOrchestration = new DirectorOrchestration(new DirectorOrchestrationConfiguration { ScenarioStages = new[] { new ScenarioStageDefinition("required", Encounter: EncounterKind.Boss) } }); var terminalCore = new DirectorCore(new DirectorConfiguration { Retry = new RetryConfiguration { MaximumAttempts = 1 } }, orchestration: terminalOrchestration); terminalCore.StartScenario(0); var terminalRequest = terminalCore.Tick(World(0)).SpawnRequests.Single(); terminalCore.ApplyResult(new(terminalRequest.RequestId, RequestResultKind.Rejected, 0, "veto")); Check(terminalCore.Tick(World(0)).Events.Any(x => x.Name == "orchestration_failed") && terminalOrchestration.ScenarioStatus == ScenarioStatus.Failed, "terminal orchestration failure is explicit");
}
static void AdaptiveCallbacks()
{
var policy = new AdaptivePolicy(); var core = new DirectorCore(new DirectorConfiguration(), policy: policy, schedule: new SingleKindSchedule(EncounterKind.Boss)); var output = core.Tick(World(0)); Check(output.SpawnRequests.Single().RequestedCount == 1 && output.MusicIntensity == .5f && output.Events.Any(x => x.Name == "boss_music"), "adaptive count music and boss callback");
var populationCore = new DirectorCore(new DirectorConfiguration(), policy: policy, schedule: new SingleKindSchedule(EncounterKind.Ambient)); Check(populationCore.Tick(World(0)).SpawnRequests.Single().RequestedCount == 1, "adaptive population limit");
var raisedLimit = new DirectorCore(new DirectorConfiguration { Population = new PopulationConfiguration { Limits = new Dictionary<EncounterKind, int> { [EncounterKind.Ambient] = 0 } } }, policy: policy, schedule: new SingleKindSchedule(EncounterKind.Ambient)); Check(raisedLimit.Tick(World(0)).SpawnRequests.Single().RequestedCount == 1, "adaptive population policy can raise configured limit");
var timing = new RuleBasedEncounterSchedule(new[] { new EncounterRule { Id = "timed", Kind = EncounterKind.Ambient, CooldownSeconds = 10, Phases = new HashSet<TempoPhase> { TempoPhase.Relax } } }, new ModeEncounterTimingPolicy(new Dictionary<string, double> { ["survival"] = .5 })); var ledger = new PopulationLedger(new PopulationConfiguration()); var timedWorld = new WorldSnapshot { Time = 0, ModeId = "survival" }; Check(timing.SelectEncounter(timedWorld, TempoPhase.Relax, 0, ledger, new DeterministicRandom(1)) is not null, "adaptive timing initial"); timing.SelectionFinalized(true); Check(timing.SelectEncounter(new WorldSnapshot { Time = 4.9, ModeId = "survival" }, TempoPhase.Relax, 0, ledger, new DeterministicRandom(1)) is null && timing.SelectEncounter(new WorldSnapshot { Time = 5, ModeId = "survival" }, TempoPhase.Relax, 0, ledger, new DeterministicRandom(1)) is not null, "mode cooldown multiplier");
var decisions = 0; var runner = new DirectorHostRunner(new DirectorCore(new DirectorConfiguration()), new ContinuousSessionFlow(), new DelegateWorldSource(() => World(0)), new DelegateSpawnExecutor(x => new(x.RequestId, RequestResultKind.Succeeded, x.RequestedCount)), notifications: new DelegateNotificationSink(_ => decisions++)); runner.Update(); Check(decisions == 1, "decision notification callback");
}
static void HostRunner()
{
var core = new DirectorCore(new DirectorConfiguration()); var flow = new ContinuousSessionFlow(); var executed = 0;
var runner = new DirectorHostRunner(core, flow, new DelegateWorldSource(() => World(0)), new DelegateSpawnExecutor(request => { executed++; return new(request.RequestId, RequestResultKind.Succeeded, request.RequestedCount); }));
var result = runner.Update(); Check(result.Status == DirectorRunStatus.Updated && executed == 1 && result.Results.Count == 1, "host executes decisions");
var denied = new DirectorHostRunner(new DirectorCore(new DirectorConfiguration()), flow, new DelegateWorldSource(() => throw new Exception("must not capture")), new DelegateSpawnExecutor(_ => throw new Exception()), new TestAuthority(false)); Check(denied.Update().Status == DirectorRunStatus.NotAuthoritative, "authority gate");
flow.IsSessionActive = false; Check(runner.Update().Status == DirectorRunStatus.SessionInactive, "session gate");
flow.IsSessionActive = true; var failureRunner = new DirectorHostRunner(new DirectorCore(new DirectorConfiguration()), flow, new DelegateWorldSource(() => World(0)), new DelegateSpawnExecutor(_ => throw new InvalidOperationException("host failure"))); Check(failureRunner.Update().Results[0].Result == RequestResultKind.Failed, "host exception becomes result");
var malformedRunner = new DirectorHostRunner(new DirectorCore(new DirectorConfiguration()), flow, new DelegateWorldSource(() => World(0)), new DelegateSpawnExecutor(request => new(request.RequestId + 99, RequestResultKind.Succeeded, request.RequestedCount + 1))); var malformed = malformedRunner.Update().Results.Single(); Check(malformed.RequestId == 1 && malformed.Result == RequestResultKind.Failed && malformed.SpawnedCount == 0, "host runner normalizes malformed result");
}
static void OwnershipAndResources()
{
var allocator = new EncounterOwnershipAllocator(); var first = allocator.Select(new[] { new OwnershipCandidate("a", 1, false), new OwnershipCandidate("b", 1) }, new DeterministicRandom(1)); Check(first == "b", "ownership eligibility");
var r1 = new DeterministicRandom(9); var r2 = new DeterministicRandom(9); var candidates = new[] { new OwnershipCandidate("a", 1), new OwnershipCandidate("b", 3) }; Check(allocator.Select(candidates, r1) == allocator.Select(candidates, r2), "ownership deterministic");
var budget = new ResourceBudget(new Dictionary<string, float> { ["health"] = 2 }); Check(budget.TrySpend("health", 1.5f) && !budget.TrySpend("health", 1), "resource budget"); Near(budget.Capture()["health"], .5f, .0001f, "resource capture");
}
static MusicEventCatalog TestMusicCatalog() => new(new[]
{
new MusicEventDefinition { EventId = "ambient.safe", TrackId = "ambient", Priority = MusicPriority.Low },
new MusicEventDefinition { EventId = "mobbed", TrackId = "mob", Priority = MusicPriority.High, DuckTrackList = new[] { "all" } },
new MusicEventDefinition { EventId = "boss", TrackId = "boss", Priority = MusicPriority.Critical, BlockTrackList = new[] { "combat" } },
new MusicEventDefinition { EventId = "combat.low", TrackId = "combat", Priority = MusicPriority.Low },
new MusicEventDefinition { EventId = "combat.high", TrackId = "combat", Priority = MusicPriority.High },
new MusicEventDefinition { EventId = "delayed", TrackId = "stinger", Priority = MusicPriority.Medium, DelaySeconds = 2 },
new MusicEventDefinition { EventId = "intro", TrackId = "intro", Priority = MusicPriority.High, AutoQueueEventId = "loop" },
new MusicEventDefinition { EventId = "loop", TrackId = "loop", Priority = MusicPriority.High },
new MusicEventDefinition { EventId = "death", TrackId = "death", Priority = MusicPriority.Critical, Flags = MusicMasterFlags.AllowAfterDeath },
new MusicEventDefinition { EventId = "tag.master", TrackId = "master", Priority = MusicPriority.High, TimingTags = new[] { new MusicTimingTag(2, 4) }, LoopSeconds = 8 },
new MusicEventDefinition { EventId = "tag.child", TrackId = "child", Priority = MusicPriority.High, SyncTrackId = "master", SyncTagIndex = 2 },
new MusicEventDefinition { EventId = "split.a", TrackId = "split", Priority = MusicPriority.High },
new MusicEventDefinition { EventId = "split.b", TrackId = "split", Priority = MusicPriority.High, Flags = MusicMasterFlags.PlaySplit }
});
static MusicDecisionBatch MusicBatch(double time, params MusicDecision[] decisions)
{
var batch = new MusicDecisionBatch { Time = time }; batch.Decisions.AddRange(decisions); return batch;
}
static void MusicAuthority()
{
var special = new SpecialMusicAlertConfiguration { ArchetypeId = "ambusher", CloseEvent = "alert.close", MiddleEvent = "alert.middle", FarEvent = "alert.far" };
var cues = new MusicCueConfiguration { CombatIntroEvents = new[] { "combat.intro" }, CombatSecondaryEvent = "combat.secondary", CombatCloseEvent = "combat.close", MobbedEvent = "mobbed", BossApproachingEvent = "boss", BossTrackId = "boss", HazardBurningEvent = "hazard.burn", HazardAttackEvent = "hazard.attack", HazardRageEvent = "hazard.rage", SafeAtmosphereEvent = "ambient.safe", DangerAtmosphereEvent = "ambient.danger", PositionalStingerEvent = "choir" };
var configuration = RecoveredMusicDirectorDefaults.CreateConfiguration(cues, new[] { special }, 41);
var core = new MusicDirectorCore(configuration);
var idle = new MusicWorldSnapshot { Time = 0, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } };
var initial = core.Tick(idle); Check(initial.Decisions.Count(x => x.Kind == MusicDecisionKind.SetMixerLayer) == 9, "music authority emits initial mixer snapshot");
var combatInput = new MusicParticipantSnapshot { Id = "p", CumulativeDamage = 10, VisibleCommonThreats = 3, ActiveCommonThreats = 20, ScannedCommonThreats = 5, VeryCloseCommonFactor = 1, AttackingCommonFactor = .4f, CloseCommonAction = .42f, InflictedDamage = true, SpecialThreats = new[] { new SpecialMusicThreat("ambusher", 1000, "s1") } };
var combat = core.Tick(new MusicWorldSnapshot { Time = 1, Participants = new[] { combatInput } });
Check(combat.Decisions.Any(x => x.Kind == MusicDecisionKind.Play && x.TargetId == "combat.intro"), "music authority starts combat group");
Check(combat.Decisions.Any(x => x.TargetId == "alert.close"), "music authority selects close special alert");
Near(combat.Decisions.Single(x => x.LayerId == MusicMixerLayers.MobBeating).Value, .37f, .0001f, "recovered damage duck equation");
Near(combat.Decisions.Single(x => x.LayerId == MusicMixerLayers.MobRules).Value, 0f, .0001f, "recovered mob pressure equation");
Near(combat.Decisions.Single(x => x.LayerId == MusicMixerLayers.CombatClose).Value, 0f, .0001f, "recovered close action equation");
var boss = core.Tick(new MusicWorldSnapshot { Time = 2, Participants = new[] { new MusicParticipantSnapshot { Id = "p", BossPresent = true } } });
Check(boss.Decisions.Any(x => x.Kind == MusicDecisionKind.Play && x.TargetId == "boss"), "music boss approach transition");
var defeated = core.Tick(new MusicWorldSnapshot { Time = 3, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } });
Check(defeated.Decisions.Any(x => x.Kind == MusicDecisionKind.StopTrack && x.TargetId == "boss" && x.FadeSeconds == 2), "music boss defeat track stop");
var hazard = core.Tick(new MusicWorldSnapshot { Time = 4, Participants = new[] { new MusicParticipantSnapshot { Id = "p", HazardPresent = true, HazardBurning = true, HazardAttacking = true, HazardRage = .75f, HazardEmitterId = "h", HazardPosition = new DirectorVector(1, 2, 3), WanderingHazards = new[] { new WanderingMusicHazard("w", .2f, new DirectorVector(4, 5, 6)) } } } });
Check(new[] { "hazard.burn", "hazard.attack", "hazard.rage" }.All(id => hazard.Decisions.Any(x => x.Kind == MusicDecisionKind.Play && x.TargetId == id)), "hazard transition cues");
Near(hazard.Decisions.Single(x => x.LayerId == MusicMixerLayers.HazardRage).Value, .25f, .0001f, "hazard rage inverse layer");
Check(hazard.Decisions.Any(x => x.TargetId == "hazard.wandering.tier4" && x.Position is not null), "wandering hazard low-anger tier and position");
var generic = core.Tick(new MusicWorldSnapshot { Time = 5, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } }, new[] { new MusicGameplayEvent(1, MusicTriggerKind.Play, "p", "custom.event") });
Check(generic.Decisions.Any(x => x.TargetId == "custom.event"), "generic gameplay music event");
var duplicate = core.Tick(new MusicWorldSnapshot { Time = 6, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } }, new[] { new MusicGameplayEvent(1, MusicTriggerKind.Play, "p", "should.not.repeat") });
Check(!duplicate.Decisions.Any(x => x.TargetId == "should.not.repeat"), "music events apply exactly once");
var reveal = core.Tick(new MusicWorldSnapshot { Time = 7, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } }, new[] { new MusicGameplayEvent(2, MusicTriggerKind.LargeAreaRevealed, "p") });
var revealLimited = core.Tick(new MusicWorldSnapshot { Time = 8, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } }, new[] { new MusicGameplayEvent(3, MusicTriggerKind.LargeAreaRevealed, "p") });
Check(reveal.Decisions.Any(x => x.TargetId == "world.large_area_reveal") && !revealLimited.Decisions.Any(x => x.TargetId == "world.large_area_reveal"), "large-area reveal cooldown");
}
static void MusicRuntimeArbitration()
{
var catalog = TestMusicCatalog(); var runtime = new MusicRuntime("p", catalog);
var ambient = runtime.Apply(MusicBatch(0, new MusicDecision(1, MusicDecisionKind.Play, "p", "ambient.safe", "test")));
Check(ambient.Actions.Any(x => x.Kind == MusicRuntimeActionKind.Play && x.EventId == "ambient.safe"), "runtime plays resolved event");
Check(runtime.Apply(MusicBatch(0, new MusicDecision(1, MusicDecisionKind.Play, "p", "ambient.safe", "duplicate"))).Actions.Count == 0, "runtime decisions apply exactly once");
var mob = runtime.Apply(MusicBatch(1, new MusicDecision(2, MusicDecisionKind.Play, "p", "mobbed", "test")));
Check(mob.Actions.Any(x => x.Kind == MusicRuntimeActionKind.SetTrackVolume && x.TrackId == "ambient" && x.Value == .5f), "runtime duck list");
Check(!mob.Actions.Any(x => x.Kind == MusicRuntimeActionKind.SetTrackVolume && x.TrackId == "mob" && x.Value == .5f), "duck source does not duck itself");
runtime.Apply(MusicBatch(2, new MusicDecision(3, MusicDecisionKind.Play, "p", "boss", "test")));
var blocked = runtime.Apply(MusicBatch(3, new MusicDecision(4, MusicDecisionKind.Play, "p", "combat.high", "test")));
Check(blocked.Results.Single(x => x.DecisionId == 4).Result == MusicDecisionResultKind.RejectedBlocked, "runtime active block list");
var priority = new MusicRuntime("p", catalog); priority.Apply(MusicBatch(0, new MusicDecision(10, MusicDecisionKind.Play, "p", "combat.low", "test")));
var high = priority.Apply(MusicBatch(1, new MusicDecision(11, MusicDecisionKind.Play, "p", "combat.high", "test")));
Check(high.Actions.Any(x => x.Kind == MusicRuntimeActionKind.StopEvent && x.EventId == "combat.low") && high.Actions.Any(x => x.Kind == MusicRuntimeActionKind.Play && x.EventId == "combat.high"), "higher priority replaces same track");
var rejectedLow = priority.Apply(MusicBatch(2, new MusicDecision(12, MusicDecisionKind.Play, "p", "combat.low", "test")));
Check(rejectedLow.Results.Single(x => x.DecisionId == 12).Result == MusicDecisionResultKind.RejectedPriority, "lower priority cannot replace active track");
var delayed = new MusicRuntime("p", catalog); var deferred = delayed.Apply(MusicBatch(0, new MusicDecision(20, MusicDecisionKind.Play, "p", "delayed", "test")));
Check(deferred.Results.Single().Result == MusicDecisionResultKind.Deferred && delayed.Tick(1).Actions.Count == 0, "runtime delay queue");
Check(delayed.Tick(2).Actions.Any(x => x.EventId == "delayed"), "runtime delayed engagement");
var tagged = new MusicRuntime("p", catalog); tagged.Apply(MusicBatch(0, new MusicDecision(30, MusicDecisionKind.Play, "p", "tag.master", "test")));
var tagDeferred = tagged.Apply(MusicBatch(1, new MusicDecision(31, MusicDecisionKind.Play, "p", "tag.child", "test")));
Check(tagDeferred.Results.Single().Result == MusicDecisionResultKind.Deferred && tagged.Tick(3.9).Actions.Count == 0 && tagged.Tick(4).Actions.Any(x => x.EventId == "tag.child"), "runtime timing-tag alignment");
var split = new MusicRuntime("p", catalog); split.Apply(MusicBatch(0, new MusicDecision(35, MusicDecisionKind.Play, "p", "split.a", "test"))); split.Apply(MusicBatch(1, new MusicDecision(36, MusicDecisionKind.Play, "p", "split.b", "test")));
Check(split.Active.Count == 2, "runtime split flag permits concurrent same-track events");
var auto = new MusicRuntime("p", catalog); var intro = auto.Apply(MusicBatch(0, new MusicDecision(40, MusicDecisionKind.Play, "p", "intro", "test"))); var instance = intro.Results.Single().InstanceId;
Check(auto.ReportCompleted(instance, 1).Actions.Any(x => x.EventId == "loop"), "runtime autoqueue on completion");
auto.SetLifecycle(true, false); var deadReject = auto.Apply(MusicBatch(2, new MusicDecision(41, MusicDecisionKind.Play, "p", "ambient.safe", "test"), new MusicDecision(42, MusicDecisionKind.Play, "p", "death", "test")));
Check(deadReject.Results.Single(x => x.DecisionId == 41).Result == MusicDecisionResultKind.RejectedLifecycle && deadReject.Results.Single(x => x.DecisionId == 42).Result == MusicDecisionResultKind.Accepted, "runtime after-death gate");
var general = auto.Apply(MusicBatch(3, new MusicDecision(43, MusicDecisionKind.General, "p", "custom.command", "test") { Arguments = new[] { "a", "b" } }));
Check(general.Actions.Single(x => x.Kind == MusicRuntimeActionKind.General).Arguments.SequenceEqual(new[] { "a", "b" }), "runtime forwards generalized commands");
}
static void MusicPersistenceAndHost()
{
var config = RecoveredMusicDirectorDefaults.CreateConfiguration(seed: 99); var first = new MusicDirectorCore(config); first.Tick(new MusicWorldSnapshot { Time = 0, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } });
first.Tick(new MusicWorldSnapshot { Time = 1, Participants = new[] { new MusicParticipantSnapshot { Id = "p", VisibleCommonThreats = 2, WeaponActivity = .5f } } });
var restored = new MusicDirectorCore(config, MusicStateCodec.DirectorFromJson(MusicStateCodec.DirectorToJson(first.CaptureState())));
var nextWorld = new MusicWorldSnapshot { Time = 2, Participants = new[] { new MusicParticipantSnapshot { Id = "p", VisibleCommonThreats = 1 } } };
Check(first.Tick(nextWorld).Decisions.SequenceEqual(restored.Tick(nextWorld).Decisions), "music authority save restore determinism");
var catalog = TestMusicCatalog(); var runtime = new MusicRuntime("p", catalog); runtime.Apply(MusicBatch(0, new MusicDecision(1, MusicDecisionKind.Play, "p", "ambient.safe", "test"), new MusicDecision(2, MusicDecisionKind.Play, "p", "delayed", "test")));
var restoredRuntime = new MusicRuntime("p", catalog, MusicStateCodec.RuntimeFromJson(MusicStateCodec.RuntimeToJson(runtime.CaptureState())));
Check(runtime.Tick(2).Actions.SequenceEqual(restoredRuntime.Tick(2).Actions), "music runtime save restore determinism");
var actions = new List<MusicRuntimeAction>(); var hostCore = new MusicDirectorCore(RecoveredMusicDirectorDefaults.CreateConfiguration(new MusicCueConfiguration { CombatIntroEvents = new[] { "ambient.safe" }, CombatSecondaryEvent = "", CombatCloseEvent = "", SafeAtmosphereEvent = "", DangerAtmosphereEvent = "", PositionalStingerEvent = "" })); var hostRuntime = new MusicRuntime("p", catalog); var runner = new MusicDirectorHostRunner(hostCore, hostRuntime, new DelegateMusicAudioSink(actions.Add));
runner.Update(new MusicWorldSnapshot { Time = 0, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } }); runner.Update(new MusicWorldSnapshot { Time = 1, Participants = new[] { new MusicParticipantSnapshot { Id = "p", VisibleCommonThreats = 1 } } });
Check(actions.Any(x => x.Kind == MusicRuntimeActionKind.Play && x.EventId == "ambient.safe"), "music host runner bridges decisions to audio sink");
}
static void Validation()
{
var backwards = new DirectorCore(new DirectorConfiguration()); backwards.Tick(World(2)); var threw = false; try { backwards.Tick(World(1)); } catch (InvalidOperationException) { threw = true; }
Check(threw, "reject backwards world time");
threw = false; try { _ = new DirectorConfiguration { MaximumRequestsPerTick = 0 }; new DirectorConfiguration { MaximumRequestsPerTick = 0 }.Validate(); } catch (ArgumentOutOfRangeException) { threw = true; }
Check(threw, "configuration validation");
threw = false; try { _ = DirectorStateCodec.FromJson("{}"); } catch (InvalidOperationException) { threw = true; }
Check(threw, "state validation");
threw = false; try { new WorldSnapshot { Participants = new[] { new ParticipantSnapshot { Id = "same" }, new ParticipantSnapshot { Id = "same" } } }.Validate(); } catch (ArgumentException) { threw = true; }
Check(threw, "duplicate participant validation");
var incomplete = new DirectorCore(new DirectorConfiguration()); var incompleteRequest = incomplete.Tick(World(0)).SpawnRequests.Single(); incomplete.ApplyResult(new(incompleteRequest.RequestId, RequestResultKind.Succeeded, 0)); Check(incomplete.CaptureState().RetryQueue.Count == 1, "incomplete success becomes partial retry");
var excessive = new DirectorCore(new DirectorConfiguration()); var excessiveRequest = excessive.Tick(World(0)).SpawnRequests.Single(); threw = false; try { excessive.ApplyResult(new(excessiveRequest.RequestId, RequestResultKind.Succeeded, excessiveRequest.RequestedCount + 1)); } catch (ArgumentOutOfRangeException) { threw = true; }
Check(threw, "excessive host result rejected");
}
static void LongDeterminism()
{
var configuration = new DirectorConfiguration { Seed = 7823, RequestTimeoutSeconds = 2 }; DirectorCore first = new(configuration); DirectorCore second = new(configuration);
for (var index = 0; index < 1000; index++)
{
var time = index * .1;
if (index % 73 == 0) { var pressure = new PressureEvent("p", (PressureSeverity)(index / 73 % 4 + 1), "simulation"); first.ApplyPressure(pressure, time); second.ApplyPressure(pressure, time); }
var world = World(time); var a = first.Tick(world); var b = second.Tick(world);
Check(a.Tempo == b.Tempo && a.TeamPressure == b.TeamPressure && a.MusicIntensity == b.MusicIntensity && a.SpawnRequests.SequenceEqual(b.SpawnRequests) && a.Events.SequenceEqual(b.Events), $"deterministic tick {index}");
foreach (var request in a.SpawnRequests) { var result = new SpawnRequestResult(request.RequestId, RequestResultKind.Succeeded, request.RequestedCount); first.ApplyResult(result); second.ApplyResult(result); }
if (index == 500) second = new DirectorCore(configuration, DirectorStateCodec.FromJson(DirectorStateCodec.ToJson(second.CaptureState())));
}
}
sealed class RejectAllPlacement : IPlacementPolicy { public bool IsAllowed(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) => false; public float ScoreAdjustment(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) => 0; }
sealed class TestAuthority : IDirectorAuthority { public TestAuthority(bool value) { IsAuthoritative = value; } public bool IsAuthoritative { get; } }
sealed class RepeatingSchedule : IEncounterSchedule { int _index; public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random) => _index++ % 2 == 0 ? EncounterKind.Ambient : EncounterKind.Special; public int RequestedCount(EncounterKind kind, WorldSnapshot world) => 1; }
sealed class SingleKindSchedule : IEncounterSchedule { readonly EncounterKind _kind; public SingleKindSchedule(EncounterKind kind) { _kind = kind; } public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random) => _kind; public int RequestedCount(EncounterKind kind, WorldSnapshot world) => 2; }
sealed class AdaptivePolicy : IDirectorPolicy, IAdaptiveDirectorPolicy, IAdaptivePopulationPolicy
{
public bool AllowEncounter(EncounterKind kind, WorldSnapshot world) => true;
public bool CanCoexist(EncounterKind requested, PopulationSnapshot population) => true;
public string SelectArchetype(EncounterKind kind, WorldSnapshot world, DeterministicRandom random) => "boss";
public int AdjustEncounterCount(EncounterKind kind, int proposedCount, TempoPhase tempo, float teamPressure, WorldSnapshot world) => 1;
public float AdjustMusicIntensity(float proposedIntensity, TempoPhase tempo, float teamPressure, WorldSnapshot world) => .5f;
public bool ShouldPlayBossMusic(EncounterKind kind, WorldSnapshot world) => true;
public int AdjustPopulationLimit(EncounterKind kind, int configuredLimit, WorldSnapshot world) => 1;
}
static void AdvancedDeterminism()
{
var configuration = new DirectorConfiguration { Seed = 99, MaximumRequestsPerTick = 3, Retry = new RetryConfiguration { MaximumAttempts = 3, DelaySeconds = .5 } };
DirectorCore first = BuildAdvancedCore(configuration); DirectorCore second = BuildAdvancedCore(configuration); first.StartScenario(0); second.StartScenario(0); first.StartWaveSequence(0, 2); second.StartWaveSequence(0, 2);
for (var index = 0; index < 500; index++)
{
var baseWorld = World(index * .25); var world = new WorldSnapshot { Time = baseWorld.Time, Participants = baseWorld.Participants, SpawnCandidates = baseWorld.SpawnCandidates, Resources = new ResourceWorldSnapshot { Candidates = new[] { new ResourceCandidate { Id = "supply-a", Category = "supply", AreaCapacity = 2 }, new ResourceCandidate { Id = "supply-b", Category = "supply", AreaCapacity = 1 } } } };
var a = first.Tick(world); var b = second.Tick(world); Check(a.SpawnRequests.SequenceEqual(b.SpawnRequests) && a.ResourceRequests.SequenceEqual(b.ResourceRequests) && a.Events.SequenceEqual(b.Events) && a.MusicIntensity == b.MusicIntensity, $"advanced deterministic tick {index}");
foreach (var request in a.SpawnRequests) { var result = new SpawnRequestResult(request.RequestId, request.RequestId % 7 == 0 ? RequestResultKind.Failed : RequestResultKind.Succeeded, request.RequestId % 7 == 0 ? 0 : request.RequestedCount); first.ApplyResult(result); second.ApplyResult(result); }
foreach (var request in a.ResourceRequests) { var result = new ResourceSpawnResult(request.RequestId, RequestResultKind.Succeeded); first.ApplyResourceResult(result); second.ApplyResourceResult(result); }
if (index == 250) second = BuildAdvancedCore(configuration, DirectorStateCodec.FromJson(DirectorStateCodec.ToJson(second.CaptureState())));
}
}
static DirectorCore BuildAdvancedCore(DirectorConfiguration configuration, DirectorState? state = null)
{
IEncounterSchedule Schedule() => new ConcurrentEncounterSchedule(new[] { new ScheduleLane { Id = "ambient", Priority = 10, Schedule = new BasicEncounterSchedule(2, 4) }, new ScheduleLane { Id = "special", Priority = 5, Schedule = new RuleBasedEncounterSchedule(new[] { new EncounterRule { Id = "special", Kind = EncounterKind.Special, Count = 1, CooldownSeconds = 5, Probability = .5f, Phases = new HashSet<TempoPhase> { TempoPhase.Relax, TempoPhase.BuildUp, TempoPhase.SustainPeak } } }) } });
var composer = new WeightedEncounterComposer(new Dictionary<EncounterKind, IReadOnlyList<WeightedArchetype>> { [EncounterKind.CommonWave] = new[] { new WeightedArchetype("grunt", 3), new WeightedArchetype("brute", 1) } });
var resources = new ResourcePopulationController(new[] { new ResourceRule { Category = "supply", TargetCount = 2, MaximumRequestsPerTick = 2 } }, 77);
var orchestrationConfiguration = new DirectorOrchestrationConfiguration { PersistenceId = "advanced-test-v1", ScenarioStages = new[] { new ScenarioStageDefinition("opening", .5), new ScenarioStageDefinition("boss", Encounter: EncounterKind.Boss), new ScenarioStageDefinition("done") }, Waves = new WaveSequenceConfiguration { WaveCount = 2, InitialDelayMinimum = 0, InitialDelayMaximum = 0, CombatTimeout = 0, PauseMinimum = .25, PauseMaximum = .25 } };
if (state is null) return new(configuration, schedule: Schedule(), composer: composer, resources: resources, orchestration: new(orchestrationConfiguration));
var orchestration = state.Value.Orchestration is { } orchestrationState ? new DirectorOrchestration(orchestrationConfiguration, orchestrationState) : null;
return new(configuration, state.Value, schedule: Schedule(), composer: composer, resources: resources, orchestration: orchestration);
}
sealed class StatefulPolicy : IDirectorPolicy, IPersistentDirectorPolicy
{
public StatefulPolicy(string id) { PersistenceId = id; }
public string PersistenceId { get; }
public int CallCount { get; private set; }
public bool AllowEncounter(EncounterKind kind, WorldSnapshot world) { CallCount++; return true; }
public bool CanCoexist(EncounterKind requested, PopulationSnapshot population) => true;
public string SelectArchetype(EncounterKind kind, WorldSnapshot world, DeterministicRandom random) => kind.ToString();
public IReadOnlyDictionary<string, string> CapturePolicyState() => new Dictionary<string, string> { ["calls"] = CallCount.ToString(CultureInfo.InvariantCulture) };
public void RestorePolicyState(IReadOnlyDictionary<string, string> state) => CallCount = int.Parse(state["calls"], CultureInfo.InvariantCulture);
}
sealed class StatefulPlacementPolicy : IPlacementPolicy, IPersistentPlacementPolicy
{
public StatefulPlacementPolicy(string id) { PersistenceId = id; }
public string PersistenceId { get; }
public int CallCount { get; private set; }
public bool IsAllowed(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) { CallCount++; return true; }
public float ScoreAdjustment(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) => 0;
public IReadOnlyDictionary<string, string> CapturePlacementState() => new Dictionary<string, string> { ["calls"] = CallCount.ToString(CultureInfo.InvariantCulture) };
public void RestorePlacementState(IReadOnlyDictionary<string, string> state) => CallCount = int.Parse(state["calls"], CultureInfo.InvariantCulture);
}
sealed class StatefulComposer : IEncounterComposer, IPersistentEncounterComposer
{
public StatefulComposer(string id) { PersistenceId = id; }
public string PersistenceId { get; }
public int CallCount { get; private set; }
public IReadOnlyList<EncounterMember> Compose(EncounterKind kind, int requestedCount, WorldSnapshot world, IDirectorPolicy policy, DeterministicRandom random) { CallCount++; return new[] { new EncounterMember("stateful", requestedCount) }; }
public IReadOnlyDictionary<string, string> CaptureComposerState() => new Dictionary<string, string> { ["calls"] = CallCount.ToString(CultureInfo.InvariantCulture) };
public void RestoreComposerState(IReadOnlyDictionary<string, string> state) => CallCount = int.Parse(state["calls"], CultureInfo.InvariantCulture);
}
}