From a0b10423ac7f952ed237818cf53dd54e72fb5686 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Sun, 10 May 2026 17:38:43 +0200 Subject: [PATCH] Expand rule event coverage --- TASKS.md | 10 +- .../LevelValidator.cs | 101 ++++++++++++- src/ReactorMaintenance.Simulation/Models.cs | 34 ++++- .../SimulationEngine.cs | 39 ++++- .../SimulationEngineTests.cs | 137 +++++++++++++++++- 5 files changed, 308 insertions(+), 13 deletions(-) diff --git a/TASKS.md b/TASKS.md index 0dbf358..66a21c5 100644 --- a/TASKS.md +++ b/TASKS.md @@ -40,6 +40,12 @@ - Added tests for T-junction ratio splits, zero-weight branches, ambiguous junction validation, and best-path flow into non-junction cells. - Verified `dotnet test tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj` passes: 15 passed. - Ran `jb cleanupcode --build=False ...` and `python D:\Code\crlf.py ...` for touched C# files after the junction slice. +- Expanded rule predicates with reactor readiness/loss/win, network value bands, and robot inventory checks. +- Expanded rule effects with removal of surface hazards, heat, and inventory, plus authored access positions for leak-start effects. +- Hardened rule event validation for predicate targets, effect targets, leak access, and non-negative amount effects. +- Added tests for network-band rules, reactor-ready rules, inventory rules, removal effects, electricity leak access, and invalid rule targets. +- Verified `dotnet test tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj` passes after the rule slice: 21 passed. +- Ran `jb cleanupcode --build=False ...` and `python D:\Code\crlf.py ...` for touched C# files after the rule slice. ## Current Work @@ -47,10 +53,10 @@ ## Future Work -1. Expand simulation fidelity where the first slice is intentionally simplified: complete pair table coverage, richer rule predicates/effects, and stronger forecast proof cases. +1. Expand simulation fidelity where the first slice is intentionally simplified: complete pair table coverage and stronger forecast proof cases. 2. Add advanced editor workflows for explicit reactor binding selection, explicit door edge selection, electricity wall leak face selection, and rule event authoring. 3. Verify and polish the Win2D app on Windows where the XAML compiler can run. 4. Update README and any affected docs to reflect the new schema, .NET target, editor controls, and deterministic defaults. 5. Build the Win2D project on a Windows-capable environment after the editor rewrite. -6. Add broader tests for junction ratios, ambiguous junctions, all rule event families, serialization edge cases, and editor operations. +6. Add broader tests for junction ratios, ambiguous junctions, serialization edge cases, and editor operations. 7. Run cleanup when `dotnet-jb` is available, tests, code review, and iterate until the implementation is clean and maintainable. diff --git a/src/ReactorMaintenance.Simulation/LevelValidator.cs b/src/ReactorMaintenance.Simulation/LevelValidator.cs index 8506753..a424ed5 100644 --- a/src/ReactorMaintenance.Simulation/LevelValidator.cs +++ b/src/ReactorMaintenance.Simulation/LevelValidator.cs @@ -121,14 +121,107 @@ public sealed class LevelValidator { foreach (var ruleEvent in level.RuleEvents) { + foreach (var predicate in ruleEvent.Predicates) + ValidateRulePredicate(level, predicate, errors); + foreach (var effect in ruleEvent.Effects) - { - if (!level.InBounds(effect.Position) && effect.Kind != ERuleEffectKind.EmitWarning && effect.Kind != ERuleEffectKind.MarkTerminalLoss && effect.Kind != ERuleEffectKind.AddInventory) - errors.Add(new("Rule effect target is out of bounds.", effect.Position)); - } + ValidateRuleEffect(level, effect, errors); } } + private static void ValidateRulePredicate(LevelState level, RulePredicate predicate, List errors) + { + switch (predicate.Kind) + { + case ERulePredicateKind.PropStateAt: + if (!level.InBounds(predicate.Position) || level.GetProp(predicate.Position).Type == EPropType.None) + errors.Add(new("Rule prop predicate must target a prop.", predicate.Position)); + break; + case ERulePredicateKind.ConsumerStateAt: + if (!IsProp(level, predicate.Position, EPropType.Consumer)) + errors.Add(new("Rule consumer predicate must target a consumer prop.", predicate.Position)); + break; + case ERulePredicateKind.NetworkBandAt: + if (!level.InBounds(predicate.Position) || !level.GetUnderground(predicate.Position, predicate.Carrier).IsPresent) + errors.Add(new("Rule network predicate must target an underground cell.", predicate.Position)); + break; + case ERulePredicateKind.SurfaceBandAt: + case ERulePredicateKind.RobotAt: + if (!level.IsFloor(predicate.Position)) + errors.Add(new("Rule floor predicate must target a floor cell.", predicate.Position)); + break; + case ERulePredicateKind.ReactorReadyIs: + case ERulePredicateKind.ReactorWonIs: + ValidateOptionalReactorId(level, predicate.ReactorId, predicate.Position, errors); + break; + } + } + + private static void ValidateRuleEffect(LevelState level, RuleEffect effect, List errors) + { + if (RequiresNonNegativeAmount(effect.Kind) && effect.Amount < 0) + errors.Add(new("Rule effect amount must be non-negative.", effect.Position)); + + switch (effect.Kind) + { + case ERuleEffectKind.StartLeak: + ValidateRuleLeakEffect(level, effect, errors); + break; + case ERuleEffectKind.WorsenLeak: + case ERuleEffectKind.RepairNetworkCell: + case ERuleEffectKind.DisableNetworkCell: + if (!level.InBounds(effect.Position) || !level.GetUnderground(effect.Position, effect.Carrier).IsPresent) + errors.Add(new("Rule network effect must target an underground cell.", effect.Position)); + break; + case ERuleEffectKind.SetPropEnabled: + if (!level.InBounds(effect.Position) || level.GetProp(effect.Position).Type == EPropType.None) + errors.Add(new("Rule prop effect must target a prop.", effect.Position)); + break; + case ERuleEffectKind.AddSurfaceHazard: + case ERuleEffectKind.RemoveSurfaceHazard: + case ERuleEffectKind.AddHeat: + case ERuleEffectKind.RemoveHeat: + if (!level.IsFloor(effect.Position)) + errors.Add(new("Rule surface effect must target a floor cell.", effect.Position)); + break; + } + } + + private static bool RequiresNonNegativeAmount(ERuleEffectKind kind) + { + return kind is ERuleEffectKind.AddSurfaceHazard + or ERuleEffectKind.RemoveSurfaceHazard + or ERuleEffectKind.AddHeat + or ERuleEffectKind.RemoveHeat + or ERuleEffectKind.AddInventory + or ERuleEffectKind.RemoveInventory; + } + + private static void ValidateRuleLeakEffect(LevelState level, RuleEffect effect, List errors) + { + var accessPosition = effect.AccessPosition ?? effect.Position; + if (!level.InBounds(effect.Position) || !level.GetUnderground(effect.Position, effect.Carrier).IsPresent) + errors.Add(new("Rule leak effect must target an underground cell.", effect.Position)); + + if (!level.IsFloor(accessPosition)) + { + errors.Add(new("Rule leak effect must have valid floor access.", accessPosition)); + return; + } + + if (effect.Carrier is ECarrierType.Fuel or ECarrierType.Coolant && effect.Position != accessPosition) + errors.Add(new("Rule fuel and coolant leak effects must use their underground coordinate as access.", accessPosition)); + + if (effect.Carrier == ECarrierType.Electricity && effect.Position.ManhattanDistance(accessPosition) != 1) + errors.Add(new("Rule electricity leak effect access must be an adjacent floor face.", accessPosition)); + } + + private static void ValidateOptionalReactorId(LevelState level, int reactorId, GridPosition position, List errors) + { + if (reactorId > 0 && level.Reactors.All(reactor => reactor.ReactorId != reactorId)) + errors.Add(new("Rule reactor predicate must reference an existing reactor.", position)); + } + private static void ValidateWarnings(LevelState level, List warnings) { foreach (var carrier in Enum.GetValues()) diff --git a/src/ReactorMaintenance.Simulation/Models.cs b/src/ReactorMaintenance.Simulation/Models.cs index 512b50f..0716a26 100644 --- a/src/ReactorMaintenance.Simulation/Models.cs +++ b/src/ReactorMaintenance.Simulation/Models.cs @@ -1,4 +1,4 @@ -namespace ReactorMaintenance.Simulation; +namespace ReactorMaintenance.Simulation; public enum ECellTerrain { @@ -108,10 +108,15 @@ public enum ERulePredicateKind { TurnAtLeast, LevelStateIs, + ReactorReadyIs, + ReactorLostIs, + ReactorWonIs, PropStateAt, ConsumerStateAt, + NetworkBandAt, SurfaceBandAt, RobotAt, + RobotInventoryAtLeast, AllSeeingEyeUnlocked } @@ -123,12 +128,21 @@ public enum ERuleEffectKind DisableNetworkCell, SetPropEnabled, AddSurfaceHazard, + RemoveSurfaceHazard, AddHeat, + RemoveHeat, AddInventory, + RemoveInventory, MarkTerminalLoss, EmitWarning } +public enum ENetworkValueKind +{ + Amount, + Intensity +} + public enum EBand { Safe, @@ -279,10 +293,10 @@ public sealed record RobotState public RobotState Add(ERemedyType remedy, int amount) { return remedy switch { - ERemedyType.FuelNeutralizer => this with { FuelNeutralizers = FuelNeutralizers + amount }, - ERemedyType.CoolantNeutralizer => this with { CoolantNeutralizers = CoolantNeutralizers + amount }, - ERemedyType.ElectricityNeutralizer => this with { ElectricityNeutralizers = ElectricityNeutralizers + amount }, - ERemedyType.HeatShield => this with { HeatShields = HeatShields + amount }, + ERemedyType.FuelNeutralizer => this with { FuelNeutralizers = ClampInventory(FuelNeutralizers + amount) }, + ERemedyType.CoolantNeutralizer => this with { CoolantNeutralizers = ClampInventory(CoolantNeutralizers + amount) }, + ERemedyType.ElectricityNeutralizer => this with { ElectricityNeutralizers = ClampInventory(ElectricityNeutralizers + amount) }, + ERemedyType.HeatShield => this with { HeatShields = ClampInventory(HeatShields + amount) }, _ => throw new ArgumentOutOfRangeException(nameof(remedy), remedy, "Unsupported remedy.") }; } @@ -291,18 +305,27 @@ public sealed record RobotState { return Count(remedy) <= 0 ? this : Add(remedy, -1); } + + private static int ClampInventory(int value) + { + return Math.Clamp(value, 0, Balancing.Current.InventoryCapacityPerRemedy); + } } public sealed record RulePredicate { public ERulePredicateKind Kind { get; init; } public GridPosition Position { get; init; } = new(0, 0); + public int ReactorId { get; init; } public int Turn { get; init; } public ELevelState LevelState { get; init; } public EPropSwitchState PropSwitchState { get; init; } public EConsumerServiceState ConsumerServiceState { get; init; } public ECarrierType Carrier { get; init; } + public ENetworkValueKind NetworkValue { get; init; } + public ERemedyType Remedy { get; init; } public EBand Band { get; init; } + public int InventoryCount { get; init; } public bool BoolValue { get; init; } } @@ -310,6 +333,7 @@ public sealed record RuleEffect { public ERuleEffectKind Kind { get; init; } public GridPosition Position { get; init; } = new(0, 0); + public GridPosition? AccessPosition { get; init; } public ECarrierType Carrier { get; init; } public ERemedyType Remedy { get; init; } public float Amount { get; init; } diff --git a/src/ReactorMaintenance.Simulation/SimulationEngine.cs b/src/ReactorMaintenance.Simulation/SimulationEngine.cs index 9b8f957..ba6668e 100644 --- a/src/ReactorMaintenance.Simulation/SimulationEngine.cs +++ b/src/ReactorMaintenance.Simulation/SimulationEngine.cs @@ -447,6 +447,24 @@ public sealed class SimulationEngine return level.InBounds(position) && level.GetProp(position) is { Type: EPropType.Consumer, Carrier: var consumerCarrier, ServiceState: EConsumerServiceState.Producing } && consumerCarrier == carrier; } + private static bool ReactorMatches(LevelState level, RulePredicate predicate, Func selector) + { + return level.Reactors.Any(reactor => MatchesReactorId(reactor, predicate.ReactorId) && selector(reactor)) == predicate.BoolValue; + } + + private static bool MatchesReactorId(ReactorBinding reactor, int reactorId) + { + return reactorId <= 0 || reactor.ReactorId == reactorId; + } + + private static bool ReactorWonMatches(LevelState level, RulePredicate predicate) + { + var won = predicate.ReactorId > 0 + ? level.Reactors.Any(reactor => reactor.ReactorId == predicate.ReactorId && reactor.Activated) + : level.Global.LevelState == ELevelState.Won || level.Reactors.Any(reactor => reactor.Activated); + return won == predicate.BoolValue; + } + private LevelState ApplyRuleEvents(LevelState level, ERuleEventPhase phase) { var next = level; @@ -472,10 +490,15 @@ public sealed class SimulationEngine return predicate.Kind switch { ERulePredicateKind.TurnAtLeast => level.Global.Turn >= predicate.Turn, ERulePredicateKind.LevelStateIs => level.Global.LevelState == predicate.LevelState, + ERulePredicateKind.ReactorReadyIs => ReactorMatches(level, predicate, reactor => reactor.Ready), + ERulePredicateKind.ReactorLostIs => (level.Global.LevelState == ELevelState.Lost) == predicate.BoolValue, + ERulePredicateKind.ReactorWonIs => ReactorWonMatches(level, predicate), ERulePredicateKind.PropStateAt => level.InBounds(predicate.Position) && level.GetProp(predicate.Position).SwitchState == predicate.PropSwitchState, ERulePredicateKind.ConsumerStateAt => level.InBounds(predicate.Position) && level.GetProp(predicate.Position).ServiceState == predicate.ConsumerServiceState, + ERulePredicateKind.NetworkBandAt => level.InBounds(predicate.Position) && NetworkBand(level.GetUnderground(predicate.Position, predicate.Carrier), predicate.Carrier, predicate.NetworkValue) >= predicate.Band, ERulePredicateKind.SurfaceBandAt => level.InBounds(predicate.Position) && SurfaceBand(level.GetSurface(predicate.Position), predicate.Carrier) >= predicate.Band, ERulePredicateKind.RobotAt => level.Robot.Position == predicate.Position, + ERulePredicateKind.RobotInventoryAtLeast => level.Robot.Count(predicate.Remedy) >= predicate.InventoryCount, ERulePredicateKind.AllSeeingEyeUnlocked => level.Global.AllSeeingEyeUnlocked == predicate.BoolValue, _ => false }; @@ -490,8 +513,11 @@ public sealed class SimulationEngine ERuleEffectKind.DisableNetworkCell => level.SetUnderground(effect.Position, effect.Carrier, new()), ERuleEffectKind.SetPropEnabled => level.SetProp(effect.Position, level.GetProp(effect.Position) with { SwitchState = effect.PropSwitchState }), ERuleEffectKind.AddSurfaceHazard => level.SetSurface(effect.Position, AddSurfaceCarrier(level.GetSurface(effect.Position), effect.Carrier, effect.Amount)), + ERuleEffectKind.RemoveSurfaceHazard => level.SetSurface(effect.Position, AddSurfaceCarrier(level.GetSurface(effect.Position), effect.Carrier, -effect.Amount)), ERuleEffectKind.AddHeat => level.SetSurface(effect.Position, level.GetSurface(effect.Position) with { Heat = level.GetSurface(effect.Position).Heat + effect.Amount }), + ERuleEffectKind.RemoveHeat => level.SetSurface(effect.Position, level.GetSurface(effect.Position) with { Heat = level.GetSurface(effect.Position).Heat - effect.Amount }), ERuleEffectKind.AddInventory => level with { Robot = level.Robot.Add(effect.Remedy, (int)effect.Amount) }, + ERuleEffectKind.RemoveInventory => level with { Robot = level.Robot.Add(effect.Remedy, -(int)effect.Amount) }, ERuleEffectKind.MarkTerminalLoss => level with { Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = string.IsNullOrWhiteSpace(effect.Message) ? "TERMINAL FAILURE" : effect.Message } }, ERuleEffectKind.EmitWarning => level with { Global = level.Global with { Warnings = [.. level.Global.Warnings, effect.Message] } }, _ => level @@ -503,7 +529,7 @@ public sealed class SimulationEngine var leak = new LeakState { Carrier = effect.Carrier, UndergroundPosition = effect.Position, - AccessPosition = effect.Position + AccessPosition = effect.AccessPosition ?? effect.Position }; return level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Leaking }) with { Leaks = [.. level.Leaks, leak] }; } @@ -625,6 +651,17 @@ public sealed class SimulationEngine }; } + private static EBand NetworkBand(UndergroundCell underground, ECarrierType carrier, ENetworkValueKind valueKind) + { + var value = valueKind == ENetworkValueKind.Amount ? underground.Amount : underground.Intensity; + return carrier switch { + ECarrierType.Fuel => BandFuel(value), + ECarrierType.Coolant => BandCoolant(value), + ECarrierType.Electricity => BandElectricity(value), + _ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.") + }; + } + private static EBand BandFuel(float value) { return Balancing.Current.Band(value, Balancing.Current.FuelCaution, Balancing.Current.FuelCritical); diff --git a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs index 189ab78..9ba8ec4 100644 --- a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs +++ b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs @@ -170,6 +170,141 @@ public sealed class SimulationEngineTests Assert.Contains(forecasts, forecast => forecast.Kind == EForecastKind.TerminalLoss); } + [Fact] + public void RuleEventCanTriggerFromNetworkBand() + { + var level = LevelState.Create("Network rule", 6, 6); + level = AddLine(level, ECarrierType.Fuel, new(2, 2), new(3, 2)); + level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel }) with { + RuleEvents = [ + new RuleEventState { + Phase = ERuleEventPhase.EndOfTurn, + Predicates = [new RulePredicate { Kind = ERulePredicateKind.NetworkBandAt, Position = new(3, 2), Carrier = ECarrierType.Fuel, NetworkValue = ENetworkValueKind.Amount, Band = EBand.Critical }], + Effects = [new RuleEffect { Kind = ERuleEffectKind.EmitWarning, Message = "fuel pressure high" }] + } + ] + }; + + var next = m_Engine.AdvanceTurn(level); + + Assert.Contains("fuel pressure high", next.Global.Warnings); + } + + [Fact] + public void RuleEventCanTriggerFromReactorReadiness() + { + var level = BuildReadyLevel() with { + RuleEvents = [ + new RuleEventState { + Phase = ERuleEventPhase.EndOfTurn, + Predicates = [new RulePredicate { Kind = ERulePredicateKind.ReactorReadyIs, ReactorId = 1, BoolValue = true }], + Effects = [new RuleEffect { Kind = ERuleEffectKind.EmitWarning, Message = "reactor ready rule" }] + } + ] + }; + + var next = m_Engine.AdvanceTurn(level); + + Assert.Contains("reactor ready rule", next.Global.Warnings); + } + + [Fact] + public void RuleEventCanTriggerFromRobotInventory() + { + var level = LevelState.Create("Inventory rule", 6, 6) with { + Robot = new() { Position = new(1, 1), FuelNeutralizers = 1 }, + RuleEvents = [ + new RuleEventState { + Phase = ERuleEventPhase.StartOfSimulation, + Predicates = [new RulePredicate { Kind = ERulePredicateKind.RobotInventoryAtLeast, Remedy = ERemedyType.FuelNeutralizer, InventoryCount = 1 }], + Effects = [new RuleEffect { Kind = ERuleEffectKind.EmitWarning, Message = "fuel kit detected" }] + } + ] + }; + + var next = m_Engine.AdvanceTurn(level); + + Assert.Contains("fuel kit detected", next.Global.Warnings); + } + + [Fact] + public void RuleEventCanRemoveHazardsHeatAndInventory() + { + var level = LevelState.Create("Remove rule", 6, 6); + level = level.SetSurface(new(2, 2), new() { Fuel = 5, Heat = 5 }) with { + Robot = new() { Position = new(1, 1), FuelNeutralizers = 2 }, + Doors = [ + new DoorState { A = new(2, 2), B = new(1, 2), State = EDoorState.Closed }, + new DoorState { A = new(2, 2), B = new(3, 2), State = EDoorState.Closed }, + new DoorState { A = new(2, 2), B = new(2, 1), State = EDoorState.Closed }, + new DoorState { A = new(2, 2), B = new(2, 3), State = EDoorState.Closed } + ], + RuleEvents = [ + new RuleEventState { + Phase = ERuleEventPhase.StartOfSimulation, + Predicates = [new RulePredicate { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }], + Effects = [ + new RuleEffect { Kind = ERuleEffectKind.RemoveSurfaceHazard, Position = new(2, 2), Carrier = ECarrierType.Fuel, Amount = 2 }, + new RuleEffect { Kind = ERuleEffectKind.RemoveHeat, Position = new(2, 2), Amount = 3 }, + new RuleEffect { Kind = ERuleEffectKind.RemoveInventory, Remedy = ERemedyType.FuelNeutralizer, Amount = 1 } + ] + } + ] + }; + + var next = m_Engine.AdvanceTurn(level); + + Assert.Equal(3, next.GetSurface(new(2, 2)).Fuel); + Assert.Equal(2, next.GetSurface(new(2, 2)).Heat); + Assert.Equal(1, next.Robot.FuelNeutralizers); + } + + [Fact] + public void RuleEventStartLeakUsesAuthoredElectricityAccessFace() + { + var level = LevelState.Create("Electricity leak rule", 6, 6); + level = level.SetTerrain(new(2, 2), ECellTerrain.Wall); + level = level.SetUnderground(new(2, 2), ECarrierType.Electricity, new() { State = EUndergroundState.Intact }) with { + RuleEvents = [ + new RuleEventState { + Phase = ERuleEventPhase.StartOfSimulation, + Predicates = [new RulePredicate { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }], + Effects = [new RuleEffect { Kind = ERuleEffectKind.StartLeak, Position = new(2, 2), AccessPosition = new(2, 3), Carrier = ECarrierType.Electricity }] + } + ] + }; + + var next = m_Engine.AdvanceTurn(level); + + Assert.Single(next.Leaks); + Assert.Equal(new(2, 3), next.Leaks[0].AccessPosition); + Assert.True(next.GetSurface(new(2, 3)).Electricity > 0); + } + + [Fact] + public void ValidatorRejectsInvalidRuleTargets() + { + var level = LevelState.Create("Invalid rules", 6, 6); + level = level.SetTerrain(new(2, 2), ECellTerrain.Wall) with { + RuleEvents = [ + new RuleEventState { + Predicates = [new RulePredicate { Kind = ERulePredicateKind.PropStateAt, Position = new(1, 1) }], + Effects = [ + new RuleEffect { Kind = ERuleEffectKind.AddSurfaceHazard, Position = new(2, 2), Carrier = ECarrierType.Fuel, Amount = 1 }, + new RuleEffect { Kind = ERuleEffectKind.RepairNetworkCell, Position = new(3, 3), Carrier = ECarrierType.Coolant } + ] + } + ] + }; + + var report = new LevelValidator().Validate(level); + + Assert.False(report.IsValid); + Assert.Contains(report.Errors, error => error.Message.Contains("Rule prop predicate", StringComparison.Ordinal)); + Assert.Contains(report.Errors, error => error.Message.Contains("Rule surface effect", StringComparison.Ordinal)); + Assert.Contains(report.Errors, error => error.Message.Contains("Rule network effect", StringComparison.Ordinal)); + } + [Fact] public void ValidatorRejectsWallHazardsAndInvalidReactorBinding() { @@ -270,4 +405,4 @@ public sealed class SimulationEngineTests } private readonly SimulationEngine m_Engine = new(); -} +} \ No newline at end of file