diff --git a/src/ReactorMaintenance.Simulation/Balancing.cs b/src/ReactorMaintenance.Simulation/Balancing.cs index 77dfca5..b6f3d2d 100644 --- a/src/ReactorMaintenance.Simulation/Balancing.cs +++ b/src/ReactorMaintenance.Simulation/Balancing.cs @@ -60,6 +60,16 @@ public abstract class Balancing return new() { Verb = ESurfaceInteractionVerb.Flow, Quantity = quantity, Amount = FlowTransferRatio }; } + public float StructuralPressureThreshold(ECarrierType carrier) + { + return carrier switch { + ECarrierType.Fuel => FuelCritical, + ECarrierType.Coolant => CoolantCritical, + ECarrierType.Electricity => ElectricityCritical, + _ => MaxValue + }; + } + private SurfaceInteractionEffect Warm(EBand rowBand, EBand colBand) { return new() { @@ -110,7 +120,6 @@ public abstract class Balancing public abstract int DefaultLevelWidth { get; } public abstract int DefaultLevelHeight { get; } public abstract int MinimumLevelSize { get; } - public abstract int ActionsPerTurn { get; } public abstract int ForecastHorizon { get; } public abstract float MinValue { get; } public abstract float MaxValue { get; } @@ -139,6 +148,9 @@ public abstract class Balancing public abstract IReadOnlyList ThreeOutflowJunctionRatios { get; } public abstract float ConsumerRequiredAmount { get; } public abstract float ConsumerRequiredIntensity { get; } + public abstract int MaxStructuralIntegrity { get; } + public abstract int StructuralIntegrityLeakThreshold { get; } + public abstract float StructuralIntegrityDamageScale { get; } public abstract float LeakBaseAmount { get; } public abstract float LeakAmountScale { get; } public abstract float LeakIntensityScale { get; } diff --git a/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs b/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs index 6cb4cd9..9c0350a 100644 --- a/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs +++ b/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs @@ -5,7 +5,6 @@ public class NormalBalancing : Balancing public override int DefaultLevelWidth => 16; public override int DefaultLevelHeight => 12; public override int MinimumLevelSize => 4; - public override int ActionsPerTurn => 3; public override int ForecastHorizon => 6; public override float MinValue => 0; public override float MaxValue => 10; @@ -48,6 +47,9 @@ public class NormalBalancing : Balancing public override float ConsumerRequiredAmount => 2.5f; public override float ConsumerRequiredIntensity => 2.5f; + public override int MaxStructuralIntegrity => 10; + public override int StructuralIntegrityLeakThreshold => 2; + public override float StructuralIntegrityDamageScale => 0.35f; public override float LeakBaseAmount => 0.5f; public override float LeakAmountScale => 0.15f; public override float LeakIntensityScale => 0.1f; diff --git a/src/ReactorMaintenance.Simulation/LevelEditor.cs b/src/ReactorMaintenance.Simulation/LevelEditor.cs index 18f747c..821877a 100644 --- a/src/ReactorMaintenance.Simulation/LevelEditor.cs +++ b/src/ReactorMaintenance.Simulation/LevelEditor.cs @@ -13,7 +13,7 @@ public static class LevelEditor EEditorTool.Wall => level.SetTerrain(position, ECellTerrain.Wall), EEditorTool.Underground => SetUnderground(level, position, command.Carrier), EEditorTool.Flow => SetCarrierProp(level, position, EPropType.Flow, command.Carrier), - EEditorTool.Consumer => SetCarrierProp(level, position, EPropType.Consumer, command.Carrier), + EEditorTool.Consumer => SetFloorProp(level, position, new() { Type = EPropType.Consumer, SwitchState = EPropSwitchState.Enabled }), EEditorTool.Junction => SetFloorProp(level, position, new() { Type = EPropType.Junction }), EEditorTool.Door => SetFloorProp(level, position, new() { Type = EPropType.Door }), EEditorTool.AllSeeingEyeTerminal => SetFloorProp(level, position, new() { Type = EPropType.AllSeeingEyeTerminal }), @@ -27,19 +27,6 @@ public static class LevelEditor }; } - public static LevelState SetDoorEdge(LevelState level, GridPosition a, GridPosition b) - { - if (!level.IsFloor(a) || !level.IsFloor(b) || a.ManhattanDistance(b) != 1) - return level; - - return level.SetProp(a, new() { Type = EPropType.Door }) with { - Doors = [ - .. level.Doors.Where(door => !SameDoorEdge(door, a, b)), - new() { A = a, B = b, State = EDoorState.Closed } - ] - }; - } - public static LevelState SetLeak(LevelState level, GridPosition undergroundPosition, GridPosition accessPosition, ECarrierType carrier) { if (!level.InBounds(undergroundPosition) || !level.IsFloor(accessPosition)) @@ -51,7 +38,10 @@ public static class LevelEditor if (carrier == ECarrierType.Electricity && undergroundPosition.ManhattanDistance(accessPosition) != 1) return level; - var next = level.SetUnderground(undergroundPosition, carrier, new() { State = EUndergroundState.Leaking }); + var next = level.SetUnderground(undergroundPosition, carrier, new() { + State = EUndergroundState.Leaking, + StructuralIntegrity = Balancing.Current.StructuralIntegrityLeakThreshold + }); return next with { Leaks = [ .. next.Leaks.Where(leak => leak.UndergroundPosition != undergroundPosition || leak.Carrier != carrier), @@ -64,32 +54,12 @@ public static class LevelEditor }; } - public static LevelState BindReactorConsumer(LevelState level, int reactorId, ECarrierType carrier, GridPosition consumerPosition) - { - if (!level.InBounds(consumerPosition) || level.GetProp(consumerPosition) is not { Type: EPropType.Consumer } prop || prop.Carrier != carrier) - return level; - - var reactors = level.Reactors.Select(reactor => reactor.ReactorId == reactorId ? BindConsumer(reactor, carrier, consumerPosition) : reactor).ToArray(); - return level with { Reactors = reactors }; - } - - public static LevelState AddRuleEvent(LevelState level, RuleEventState ruleEvent) - { - var id = string.IsNullOrWhiteSpace(ruleEvent.Id) ? NextRuleEventId(level) : ruleEvent.Id; - var authoredEvent = ruleEvent with { Id = id }; - return level with { - RuleEvents = [.. level.RuleEvents.Where(existing => existing.Id != id), authoredEvent] - }; - } - - public static LevelState RemoveRuleEvent(LevelState level, string id) - { - return level with { RuleEvents = level.RuleEvents.Where(ruleEvent => ruleEvent.Id != id).ToArray() }; - } - private static LevelState SetUnderground(LevelState level, GridPosition position, ECarrierType carrier) { - return level.SetUnderground(position, carrier, new() { State = EUndergroundState.Intact }); + return level.SetUnderground(position, carrier, new() { + State = EUndergroundState.Intact, + StructuralIntegrity = Balancing.Current.MaxStructuralIntegrity + }); } private static LevelState SetCarrierProp(LevelState level, GridPosition position, EPropType type, ECarrierType carrier) @@ -125,10 +95,7 @@ public static class LevelEditor .. level.Reactors, new() { ReactorId = id, - ControlPosition = position, - FuelConsumerPosition = position, - CoolantConsumerPosition = position, - ElectricityConsumerPosition = position + ControlPosition = position } ] }; @@ -141,28 +108,4 @@ public static class LevelEditor return SetLeak(level, position, position, carrier); } - - private static bool SameDoorEdge(DoorState door, GridPosition a, GridPosition b) - { - return (door.A == a && door.B == b) || (door.A == b && door.B == a); - } - - private static ReactorBinding BindConsumer(ReactorBinding reactor, ECarrierType carrier, GridPosition consumerPosition) - { - return carrier switch { - ECarrierType.Fuel => reactor with { FuelConsumerPosition = consumerPosition }, - ECarrierType.Coolant => reactor with { CoolantConsumerPosition = consumerPosition }, - ECarrierType.Electricity => reactor with { ElectricityConsumerPosition = consumerPosition }, - _ => reactor - }; - } - - private static string NextRuleEventId(LevelState level) - { - var next = level.RuleEvents.Count + 1; - while (level.RuleEvents.Any(ruleEvent => ruleEvent.Id == $"rule-{next}")) - next++; - - return $"rule-{next}"; - } } \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/LevelSerializer.cs b/src/ReactorMaintenance.Simulation/LevelSerializer.cs index 65257c8..824e51c 100644 --- a/src/ReactorMaintenance.Simulation/LevelSerializer.cs +++ b/src/ReactorMaintenance.Simulation/LevelSerializer.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Serialization; namespace ReactorMaintenance.Simulation; @@ -33,7 +33,7 @@ public static class LevelSerializer return level; } - private const int c_CurrentVersion = 2; + private const int c_CurrentVersion = 3; private static readonly JsonSerializerOptions s_Options = new() { WriteIndented = true, diff --git a/src/ReactorMaintenance.Simulation/LevelStateExtensions.cs b/src/ReactorMaintenance.Simulation/LevelStateExtensions.cs index ad821aa..c5ae60f 100644 --- a/src/ReactorMaintenance.Simulation/LevelStateExtensions.cs +++ b/src/ReactorMaintenance.Simulation/LevelStateExtensions.cs @@ -42,7 +42,7 @@ public static class LevelStateExtensions public static bool IsClosedDoorEdge(this LevelState level, GridPosition a, GridPosition b) { - return level.Doors.Any(door => door.State == EDoorState.Closed && SameEdge(door.A, door.B, a, b)); + return DoorBlocksEdge(level, a, b) || DoorBlocksEdge(level, b, a); } public static LevelState SetTerrain(this LevelState level, GridPosition position, ECellTerrain terrain) @@ -109,8 +109,31 @@ public static class LevelStateExtensions .SetUnderground(position, ECarrierType.Electricity, new()); } - private static bool SameEdge(GridPosition edgeA, GridPosition edgeB, GridPosition a, GridPosition b) + private static bool DoorBlocksEdge(LevelState level, GridPosition doorPosition, GridPosition neighbor) { - return (edgeA == a && edgeB == b) || (edgeA == b && edgeB == a); + if (!level.InBounds(doorPosition) || !level.InBounds(neighbor)) + return false; + + var prop = level.GetProp(doorPosition); + if (prop is not { Type: EPropType.Door, DoorState: EDoorState.Closed } || doorPosition.ManhattanDistance(neighbor) != 1) + return false; + + var north = new GridPosition(doorPosition.X, doorPosition.Y - 1); + var south = new GridPosition(doorPosition.X, doorPosition.Y + 1); + var west = new GridPosition(doorPosition.X - 1, doorPosition.Y); + var east = new GridPosition(doorPosition.X + 1, doorPosition.Y); + + if (IsWall(level, north) && IsWall(level, south)) + return neighbor.Y == doorPosition.Y; + + if (IsWall(level, west) && IsWall(level, east)) + return neighbor.X == doorPosition.X; + + return false; + } + + private static bool IsWall(LevelState level, GridPosition position) + { + return level.InBounds(position) && level.GetTerrain(position) == ECellTerrain.Wall; } } \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/LevelValidator.cs b/src/ReactorMaintenance.Simulation/LevelValidator.cs index a424ed5..1b298ac 100644 --- a/src/ReactorMaintenance.Simulation/LevelValidator.cs +++ b/src/ReactorMaintenance.Simulation/LevelValidator.cs @@ -14,7 +14,6 @@ public sealed class LevelValidator ValidateLeaks(level, errors); ValidateReactors(level, errors, warnings); ValidateJunctions(level, errors); - ValidateRuleEvents(level, errors); ValidateWarnings(level, warnings); return new() { Errors = errors, Warnings = warnings }; @@ -60,10 +59,21 @@ public sealed class LevelValidator private static void ValidateDoors(LevelState level, List errors) { - foreach (var door in level.Doors) + foreach (var position in LevelTraversal.AllPositions(level)) { - if (!level.IsFloor(door.A) || !level.IsFloor(door.B) || door.A.ManhattanDistance(door.B) != 1) - errors.Add(new("Door edge must connect two adjacent floor cells.", door.A)); + if (level.GetProp(position).Type != EPropType.Door) + continue; + + if (!level.IsFloor(position)) + { + errors.Add(new("Door prop must be placed on a floor cell.", position)); + continue; + } + + var northSouthWalls = IsWall(level, new(position.X, position.Y - 1)) && IsWall(level, new(position.X, position.Y + 1)); + var westEastWalls = IsWall(level, new(position.X - 1, position.Y)) && IsWall(level, new(position.X + 1, position.Y)); + if (northSouthWalls == westEastWalls) + errors.Add(new("Door must be surrounded by one opposing pair of wall cells.", position)); } } @@ -94,21 +104,14 @@ public sealed class LevelValidator foreach (var reactor in level.Reactors) { if (!IsProp(level, reactor.ControlPosition, EPropType.ReactorControl)) - errors.Add(new("Reactor binding control position must point to a reactor control prop.", reactor.ControlPosition)); - - ValidateConsumerBinding(level, reactor.FuelConsumerPosition, ECarrierType.Fuel, errors); - ValidateConsumerBinding(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant, errors); - ValidateConsumerBinding(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity, errors); + errors.Add(new("Reactor control position must point to a reactor control prop.", reactor.ControlPosition)); if (!reactor.Ready) warnings.Add(new("Reactor is initially unready.", reactor.ControlPosition)); } - } - private static void ValidateConsumerBinding(LevelState level, GridPosition position, ECarrierType carrier, List errors) - { - if (!level.InBounds(position) || level.GetProp(position) is not { Type: EPropType.Consumer } prop || prop.Carrier != carrier) - errors.Add(new($"Missing or invalid {carrier} consumer binding.", position)); + if (level.RequiredFuelConsumers < 0 || level.RequiredCoolantConsumers < 0 || level.RequiredElectricityConsumers < 0) + errors.Add(new("Required consumer counts cannot be negative.")); } private static void ValidateJunctions(LevelState level, List errors) @@ -117,111 +120,6 @@ public sealed class LevelValidator errors.AddRange(junction.Errors.Select(error => new ValidationIssue(error, junction.Position))); } - private static void ValidateRuleEvents(LevelState level, List errors) - { - foreach (var ruleEvent in level.RuleEvents) - { - foreach (var predicate in ruleEvent.Predicates) - ValidateRulePredicate(level, predicate, errors); - - foreach (var effect in ruleEvent.Effects) - 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()) @@ -243,12 +141,31 @@ public sealed class LevelValidator { var position = new GridPosition(x, y); var prop = level.GetProp(position); - if (prop.Type == EPropType.Consumer && prop.SwitchState == EPropSwitchState.Enabled && !HasSourcePath(level, position, prop.Carrier)) - warnings.Add(new("Enabled consumer is initially starved.", position)); + if (prop.Type != EPropType.Consumer || prop.SwitchState != EPropSwitchState.Enabled) + continue; + + var hasPresentNetwork = false; + foreach (var carrier in Enum.GetValues()) + { + if (!level.GetUnderground(position, carrier).IsPresent) + continue; + + hasPresentNetwork = true; + if (!HasSourcePath(level, position, carrier)) + warnings.Add(new($"Enabled consumer has no {carrier} source path.", position)); + } + + if (!hasPresentNetwork) + warnings.Add(new("Enabled consumer has no underground network beneath it.", position)); } } } + private static bool IsWall(LevelState level, GridPosition position) + { + return level.InBounds(position) && level.GetTerrain(position) == ECellTerrain.Wall; + } + private static bool HasSourcePath(LevelState level, GridPosition start, ECarrierType carrier) { if (!level.GetUnderground(start, carrier).CarriesFlow) diff --git a/src/ReactorMaintenance.Simulation/Models/DoorState.cs b/src/ReactorMaintenance.Simulation/Models/DoorState.cs deleted file mode 100644 index 70f2dc9..0000000 --- a/src/ReactorMaintenance.Simulation/Models/DoorState.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -public sealed record DoorState -{ - public GridPosition A { get; init; } = new(0, 0); - public GridPosition B { get; init; } = new(0, 0); - public EDoorState State { get; init; } = EDoorState.Closed; -} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Models/EForecastKind.cs b/src/ReactorMaintenance.Simulation/Models/EForecastKind.cs index 7ba1c58..706d403 100644 --- a/src/ReactorMaintenance.Simulation/Models/EForecastKind.cs +++ b/src/ReactorMaintenance.Simulation/Models/EForecastKind.cs @@ -6,5 +6,5 @@ public enum EForecastKind ReactorReady, ConsumerStarved, HazardGrowth, - RuleEvent + StructuralIntegrity } \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Models/ERuleEffectKind.cs b/src/ReactorMaintenance.Simulation/Models/ERuleEffectKind.cs deleted file mode 100644 index c869f77..0000000 --- a/src/ReactorMaintenance.Simulation/Models/ERuleEffectKind.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -public enum ERuleEffectKind -{ - StartLeak, - WorsenLeak, - RepairNetworkCell, - DisableNetworkCell, - SetPropEnabled, - AddSurfaceHazard, - RemoveSurfaceHazard, - AddHeat, - RemoveHeat, - AddInventory, - RemoveInventory, - MarkTerminalLoss, - EmitWarning -} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Models/ERuleEventPhase.cs b/src/ReactorMaintenance.Simulation/Models/ERuleEventPhase.cs deleted file mode 100644 index b5a4f17..0000000 --- a/src/ReactorMaintenance.Simulation/Models/ERuleEventPhase.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -public enum ERuleEventPhase -{ - StartOfSimulation, - EndOfTurn -} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Models/ERulePredicateKind.cs b/src/ReactorMaintenance.Simulation/Models/ERulePredicateKind.cs deleted file mode 100644 index 7d75d8c..0000000 --- a/src/ReactorMaintenance.Simulation/Models/ERulePredicateKind.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -public enum ERulePredicateKind -{ - TurnAtLeast, - LevelStateIs, - ReactorReadyIs, - ReactorLostIs, - ReactorWonIs, - PropStateAt, - ConsumerStateAt, - NetworkBandAt, - SurfaceBandAt, - RobotAt, - RobotInventoryAtLeast, - AllSeeingEyeUnlocked -} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Models/GlobalState.cs b/src/ReactorMaintenance.Simulation/Models/GlobalState.cs index e371edb..79e0c6e 100644 --- a/src/ReactorMaintenance.Simulation/Models/GlobalState.cs +++ b/src/ReactorMaintenance.Simulation/Models/GlobalState.cs @@ -3,10 +3,8 @@ public sealed record GlobalState { public int Turn { get; init; } - public int ActionsRemaining { get; init; } = Balancing.Current.ActionsPerTurn; public ELevelState LevelState { get; init; } = ELevelState.Stable; public string Status { get; init; } = "STABLE"; - public bool AllSeeingEyeUnlocked { get; init; } public bool TerminalLoss { get; init; } public IReadOnlyList Warnings { get; init; } = Array.Empty(); } \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Models/LevelState.cs b/src/ReactorMaintenance.Simulation/Models/LevelState.cs index 6dffcd7..939ca9f 100644 --- a/src/ReactorMaintenance.Simulation/Models/LevelState.cs +++ b/src/ReactorMaintenance.Simulation/Models/LevelState.cs @@ -16,10 +16,11 @@ public sealed record LevelState public UndergroundCell[] Electricity { get; init; } = LevelStateFactory.CreateUnderground(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight); public SurfaceState[] Surface { get; init; } = LevelStateFactory.CreateSurface(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight); public PropState[] Props { get; init; } = LevelStateFactory.CreateProps(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight); - public IReadOnlyList Doors { get; init; } = Array.Empty(); public IReadOnlyList Leaks { get; init; } = Array.Empty(); - public IReadOnlyList Reactors { get; init; } = Array.Empty(); - public IReadOnlyList RuleEvents { get; init; } = Array.Empty(); + public IReadOnlyList Reactors { get; init; } = Array.Empty(); + public int RequiredFuelConsumers { get; init; } = 1; + public int RequiredCoolantConsumers { get; init; } = 1; + public int RequiredElectricityConsumers { get; init; } = 1; public RobotState Robot { get; init; } = new(); public GlobalState Global { get; init; } = new(); public IReadOnlyList Forecasts { get; init; } = Array.Empty(); diff --git a/src/ReactorMaintenance.Simulation/Models/PropState.cs b/src/ReactorMaintenance.Simulation/Models/PropState.cs index 090423d..d70447e 100644 --- a/src/ReactorMaintenance.Simulation/Models/PropState.cs +++ b/src/ReactorMaintenance.Simulation/Models/PropState.cs @@ -6,10 +6,24 @@ public sealed record PropState public ECarrierType Carrier { get; init; } public EPropSwitchState SwitchState { get; init; } = EPropSwitchState.Enabled; public EConsumerServiceState ServiceState { get; init; } = EConsumerServiceState.Unknown; + public EConsumerServiceState FuelServiceState { get; init; } = EConsumerServiceState.Unknown; + public EConsumerServiceState CoolantServiceState { get; init; } = EConsumerServiceState.Unknown; + public EConsumerServiceState ElectricityServiceState { get; init; } = EConsumerServiceState.Unknown; public int JunctionMode { get; init; } public ERemedyType RemedyType { get; init; } public bool Depleted { get; init; } public int ReactorId { get; init; } + public EDoorState DoorState { get; init; } = EDoorState.Closed; public bool IsEnabled => SwitchState == EPropSwitchState.Enabled; + + public EConsumerServiceState ServiceStateFor(ECarrierType carrier) + { + return carrier switch { + ECarrierType.Fuel => FuelServiceState, + ECarrierType.Coolant => CoolantServiceState, + ECarrierType.Electricity => ElectricityServiceState, + _ => EConsumerServiceState.Unknown + }; + } } \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Models/ReactorBinding.cs b/src/ReactorMaintenance.Simulation/Models/ReactorBinding.cs deleted file mode 100644 index 56932fd..0000000 --- a/src/ReactorMaintenance.Simulation/Models/ReactorBinding.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -public sealed record ReactorBinding -{ - public int ReactorId { get; init; } - public GridPosition ControlPosition { get; init; } = new(0, 0); - public GridPosition FuelConsumerPosition { get; init; } = new(0, 0); - public GridPosition CoolantConsumerPosition { get; init; } = new(0, 0); - public GridPosition ElectricityConsumerPosition { get; init; } = new(0, 0); - public bool Ready { get; init; } - public bool Activated { get; init; } -} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Models/ReactorState.cs b/src/ReactorMaintenance.Simulation/Models/ReactorState.cs new file mode 100644 index 0000000..9ed50be --- /dev/null +++ b/src/ReactorMaintenance.Simulation/Models/ReactorState.cs @@ -0,0 +1,9 @@ +namespace ReactorMaintenance.Simulation; + +public sealed record ReactorState +{ + public int ReactorId { get; init; } + public GridPosition ControlPosition { get; init; } = new(0, 0); + public bool Ready { get; init; } + public bool Activated { get; init; } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Models/RuleEffect.cs b/src/ReactorMaintenance.Simulation/Models/RuleEffect.cs deleted file mode 100644 index 37a406e..0000000 --- a/src/ReactorMaintenance.Simulation/Models/RuleEffect.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -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; } - public EPropSwitchState PropSwitchState { get; init; } - public string Message { get; init; } = string.Empty; -} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Models/RuleEventState.cs b/src/ReactorMaintenance.Simulation/Models/RuleEventState.cs deleted file mode 100644 index afbf960..0000000 --- a/src/ReactorMaintenance.Simulation/Models/RuleEventState.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -public sealed record RuleEventState -{ - public string Id { get; init; } = string.Empty; - public bool Enabled { get; init; } = true; - public bool Repeat { get; init; } - public bool Triggered { get; init; } - public int Priority { get; init; } - public ERuleEventPhase Phase { get; init; } - public IReadOnlyList Predicates { get; init; } = Array.Empty(); - public IReadOnlyList Effects { get; init; } = Array.Empty(); - public string ForecastText { get; init; } = string.Empty; -} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Models/RulePredicate.cs b/src/ReactorMaintenance.Simulation/Models/RulePredicate.cs deleted file mode 100644 index 08113b5..0000000 --- a/src/ReactorMaintenance.Simulation/Models/RulePredicate.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -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; } -} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Models/UndergroundCell.cs b/src/ReactorMaintenance.Simulation/Models/UndergroundCell.cs index bd01198..094e979 100644 --- a/src/ReactorMaintenance.Simulation/Models/UndergroundCell.cs +++ b/src/ReactorMaintenance.Simulation/Models/UndergroundCell.cs @@ -5,6 +5,7 @@ public sealed record UndergroundCell public EUndergroundState State { get; init; } public float Amount { get; init; } public float Intensity { get; init; } + public int StructuralIntegrity { get; init; } = Balancing.Current.MaxStructuralIntegrity; public bool IsPresent => State != EUndergroundState.Absent; public bool CarriesFlow => State is EUndergroundState.Intact or EUndergroundState.Leaking; diff --git a/src/ReactorMaintenance.Simulation/SimulationEngine.cs b/src/ReactorMaintenance.Simulation/SimulationEngine.cs index a473ab1..273aa8c 100644 --- a/src/ReactorMaintenance.Simulation/SimulationEngine.cs +++ b/src/ReactorMaintenance.Simulation/SimulationEngine.cs @@ -4,22 +4,27 @@ public sealed class SimulationEngine { public LevelState MoveRobot(LevelState level, GridPosition destination) { - return PlayerActionSystem.MoveRobot(level, destination, SpendAction); + return PlayerActionSystem.MoveRobot(level, destination); } public LevelState InteractProp(LevelState level) { - return PlayerActionSystem.InteractProp(level, SpendAction); + return PlayerActionSystem.InteractProp(level, ResolveStep); } public LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy) { - return PlayerActionSystem.InteractLeak(level, carrier, useRemedy, SpendAction); + return PlayerActionSystem.InteractLeak(level, carrier, useRemedy, ResolveStep); } public LevelState ApplyHeatShield(LevelState level) { - return PlayerActionSystem.ApplyHeatShield(level, SpendAction); + return PlayerActionSystem.ApplyHeatShield(level, ResolveStep); + } + + private LevelState ResolveStep(LevelState level) + { + return ResolveStep(level, true); } public LevelState ActivateReactor(LevelState level) @@ -29,45 +34,37 @@ public sealed class SimulationEngine public LevelState EndTurn(LevelState level) { - return ResolveTurn(level with { Global = level.Global with { ActionsRemaining = 0 } }); + return ResolveStep(level); } public LevelState AdvanceTurn(LevelState level) { - return ResolveTurn(level); + return ResolveStep(level); } public IReadOnlyList Forecast(LevelState level) { - return ForecastSystem.Forecast(level, simulated => ResolveTurn(simulated, false)); + return ForecastSystem.Forecast(level, simulated => ResolveStep(simulated, false)); } - private LevelState SpendAction(LevelState level) - { - var actions = Math.Max(0, level.Global.ActionsRemaining - 1); - var next = level with { Global = level.Global with { ActionsRemaining = actions } }; - return actions == 0 ? ResolveTurn(next) : next; - } - - private LevelState ResolveTurn(LevelState level, bool refreshForecasts = true) + private LevelState ResolveStep(LevelState level, bool refreshForecasts) { var report = m_Validator.Validate(level); if (!report.IsValid) return level with { Global = level.Global with { LevelState = ELevelState.Lost, Status = report.Errors[0].Message } }; - var next = RuleEventSystem.Apply(level, ERuleEventPhase.StartOfSimulation); + var next = level; next = NetworkPropagationSystem.Propagate(next); next = ConsumerSystem.Resolve(next); + next = StructuralIntegritySystem.Resolve(next); next = LeakSystem.Inject(next); next = SurfaceInteractionSystem.Resolve(next); next = RobotSafetySystem.Resolve(next); next = ReactorSystem.DeriveState(next); - next = RuleEventSystem.Apply(next, ERuleEventPhase.EndOfTurn); next = SurfaceInteractionSystem.AdvanceDurations(next); next = next with { Global = next.Global with { - Turn = next.Global.Turn + 1, - ActionsRemaining = Balancing.Current.ActionsPerTurn + Turn = next.Global.Turn + 1 } }; diff --git a/src/ReactorMaintenance.Simulation/Systems/ConsumerSystem.cs b/src/ReactorMaintenance.Simulation/Systems/ConsumerSystem.cs index 4a5ade3..5dfc984 100644 --- a/src/ReactorMaintenance.Simulation/Systems/ConsumerSystem.cs +++ b/src/ReactorMaintenance.Simulation/Systems/ConsumerSystem.cs @@ -14,15 +14,59 @@ internal static class ConsumerSystem if (prop.SwitchState == EPropSwitchState.Disabled) { - props[index] = prop with { ServiceState = EConsumerServiceState.Disabled }; + var disabledFuel = DisabledServiceStateFor(level, position, ECarrierType.Fuel); + var disabledCoolant = DisabledServiceStateFor(level, position, ECarrierType.Coolant); + var disabledElectricity = DisabledServiceStateFor(level, position, ECarrierType.Electricity); + props[index] = prop with { + ServiceState = Aggregate(disabledFuel, disabledCoolant, disabledElectricity), + FuelServiceState = disabledFuel, + CoolantServiceState = disabledCoolant, + ElectricityServiceState = disabledElectricity + }; continue; } - var underground = level.GetUnderground(position, prop.Carrier); - var supplied = underground.Amount >= Balancing.Current.ConsumerRequiredAmount && underground.Intensity >= Balancing.Current.ConsumerRequiredIntensity; - props[index] = prop with { ServiceState = supplied ? EConsumerServiceState.Producing : EConsumerServiceState.Starved }; + var fuel = ServiceStateFor(level, position, ECarrierType.Fuel); + var coolant = ServiceStateFor(level, position, ECarrierType.Coolant); + var electricity = ServiceStateFor(level, position, ECarrierType.Electricity); + props[index] = prop with { + ServiceState = Aggregate(fuel, coolant, electricity), + FuelServiceState = fuel, + CoolantServiceState = coolant, + ElectricityServiceState = electricity + }; } return level with { Props = props }; } + + private static EConsumerServiceState ServiceStateFor(LevelState level, GridPosition position, ECarrierType carrier) + { + var underground = level.GetUnderground(position, carrier); + if (!underground.IsPresent) + return EConsumerServiceState.Unknown; + + var supplied = underground.Amount >= Balancing.Current.ConsumerRequiredAmount && underground.Intensity >= Balancing.Current.ConsumerRequiredIntensity; + return supplied ? EConsumerServiceState.Producing : EConsumerServiceState.Starved; + } + + private static EConsumerServiceState DisabledServiceStateFor(LevelState level, GridPosition position, ECarrierType carrier) + { + return level.GetUnderground(position, carrier).IsPresent ? EConsumerServiceState.Disabled : EConsumerServiceState.Unknown; + } + + private static EConsumerServiceState Aggregate(params EConsumerServiceState[] states) + { + var participating = states.Where(state => state != EConsumerServiceState.Unknown).ToArray(); + if (participating.Length == 0) + return EConsumerServiceState.Unknown; + + if (participating.Any(state => state == EConsumerServiceState.Starved)) + return EConsumerServiceState.Starved; + + if (participating.Any(state => state == EConsumerServiceState.Disabled)) + return EConsumerServiceState.Disabled; + + return EConsumerServiceState.Producing; + } } \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Systems/ForecastSystem.cs b/src/ReactorMaintenance.Simulation/Systems/ForecastSystem.cs index 305abe9..a5f2a5a 100644 --- a/src/ReactorMaintenance.Simulation/Systems/ForecastSystem.cs +++ b/src/ReactorMaintenance.Simulation/Systems/ForecastSystem.cs @@ -44,15 +44,25 @@ internal static class ForecastSystem foreach (var position in LevelTraversal.AllPositions(level)) { var prop = level.GetProp(position); - if (prop.Type == EPropType.Consumer && prop.ServiceState == EConsumerServiceState.Starved) - forecasts.Add(new(EForecastKind.ConsumerStarved, position, turn, $"{prop.Carrier} consumer starved")); + if (prop.Type == EPropType.Consumer) + { + foreach (var carrier in Enum.GetValues()) + { + if (prop.ServiceStateFor(carrier) == EConsumerServiceState.Starved) + forecasts.Add(new(EForecastKind.ConsumerStarved, position, turn, $"{carrier} consumer starved")); + } + } + + foreach (var carrier in Enum.GetValues()) + { + var underground = level.GetUnderground(position, carrier); + if (underground.IsPresent && underground.StructuralIntegrity <= Balancing.Current.StructuralIntegrityLeakThreshold) + forecasts.Add(new(EForecastKind.StructuralIntegrity, position, turn, $"{carrier} structural integrity failing")); + } var surface = level.GetSurface(position); if (SimulationBands.Fuel(surface.Fuel) == EBand.Critical || SimulationBands.Coolant(surface.Coolant) == EBand.Critical || SimulationBands.Electricity(surface.Electricity) == EBand.Critical || SimulationBands.Heat(surface.Heat) == EBand.Critical) forecasts.Add(new(EForecastKind.HazardGrowth, position, turn, "Critical hazard")); } - - foreach (var ruleEvent in level.RuleEvents.Where(ruleEvent => ruleEvent.Enabled && !string.IsNullOrWhiteSpace(ruleEvent.ForecastText) && ruleEvent.Predicates.All(predicate => RuleEventSystem.PredicateMatches(level, predicate)))) - forecasts.Add(new(EForecastKind.RuleEvent, null, turn, ruleEvent.ForecastText)); } } \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Systems/PlayerActionSystem.cs b/src/ReactorMaintenance.Simulation/Systems/PlayerActionSystem.cs index 07501d2..1821bb8 100644 --- a/src/ReactorMaintenance.Simulation/Systems/PlayerActionSystem.cs +++ b/src/ReactorMaintenance.Simulation/Systems/PlayerActionSystem.cs @@ -2,23 +2,23 @@ internal static class PlayerActionSystem { - public static LevelState MoveRobot(LevelState level, GridPosition destination, Func spendAction) + public static LevelState MoveRobot(LevelState level, GridPosition destination) { - if (!CanSpendAction(level) || !level.IsFloor(destination) || level.Robot.Position.ManhattanDistance(destination) != 1) + if (!CanAct(level) || !level.IsFloor(destination) || level.Robot.Position.ManhattanDistance(destination) != 1) return Refuse(level, "MOVE BLOCKED"); - return spendAction(level with { + return level with { Robot = level.Robot with { Position = destination, HeatImmunitySteps = Math.Max(0, level.Robot.HeatImmunitySteps - 1) } - }); + }; } - public static LevelState InteractProp(LevelState level, Func spendAction) + public static LevelState InteractProp(LevelState level, Func resolveLengthyAction) { - if (!CanSpendAction(level)) - return Refuse(level, "NO ACTIONS"); + if (!CanAct(level)) + return Refuse(level, "NO CONTROL"); var position = level.Robot.Position; var prop = level.GetProp(position); @@ -28,20 +28,20 @@ internal static class PlayerActionSystem var next = prop.Type switch { EPropType.Flow or EPropType.Consumer => ToggleProp(level, position, prop), EPropType.Junction => CycleJunctionMode(level, position, prop), - EPropType.Door => ToggleDoor(level, position), - EPropType.AllSeeingEyeTerminal => level with { Global = level.Global with { AllSeeingEyeUnlocked = true, Status = "ALL-SEEING-EYE ONLINE" } }, + EPropType.Door => ToggleDoor(level, position, prop), + EPropType.AllSeeingEyeTerminal => level with { Global = level.Global with { Status = "ALL-SEEING-EYE AVAILABLE" } }, EPropType.RemedySupply => PickUpRemedy(level, position, prop), EPropType.ReactorControl => ReactorSystem.Activate(level), _ => level }; - return spendAction(next); + return prop.Type == EPropType.AllSeeingEyeTerminal || prop.Type == EPropType.ReactorControl ? next : resolveLengthyAction(next); } - public static LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy, Func spendAction) + public static LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy, Func resolveLengthyAction) { - if (!CanSpendAction(level)) - return Refuse(level, "NO ACTIONS"); + if (!CanAct(level)) + return Refuse(level, "NO CONTROL"); var leakIndex = level.Leaks.ToList().FindIndex(leak => !leak.Repaired && leak.Carrier == carrier && leak.AccessPosition == level.Robot.Position); if (leakIndex < 0) @@ -49,15 +49,15 @@ internal static class PlayerActionSystem var leak = level.Leaks[leakIndex]; var next = useRemedy ? ApplyElementRemedy(level, leak) : RepairLeak(level, leakIndex, leak); - return spendAction(next); + return resolveLengthyAction(next); } - public static LevelState ApplyHeatShield(LevelState level, Func spendAction) + public static LevelState ApplyHeatShield(LevelState level, Func resolveLengthyAction) { - if (!CanSpendAction(level) || level.Robot.HeatShields <= 0) + if (!CanAct(level) || level.Robot.HeatShields <= 0) return Refuse(level, "NO HEAT SHIELD"); - return spendAction(level with { + return resolveLengthyAction(level with { Robot = level.Robot.Spend(ERemedyType.HeatShield) with { HeatImmunitySteps = Balancing.Current.HeatShieldSteps } }); } @@ -68,15 +68,10 @@ internal static class PlayerActionSystem return level.SetProp(position, prop with { SwitchState = switchState }); } - private static LevelState ToggleDoor(LevelState level, GridPosition position) + private static LevelState ToggleDoor(LevelState level, GridPosition position, PropState prop) { - var doors = level.Doors.ToArray(); - var index = Array.FindIndex(doors, door => door.A == position || door.B == position); - if (index < 0) - return level; - - doors[index] = doors[index] with { State = doors[index].State == EDoorState.Open ? EDoorState.Closed : EDoorState.Open }; - return level with { Doors = doors }; + var doorState = prop.DoorState == EDoorState.Open ? EDoorState.Closed : EDoorState.Open; + return level.SetProp(position, prop with { DoorState = doorState }); } private static LevelState PickUpRemedy(LevelState level, GridPosition position, PropState prop) @@ -91,7 +86,12 @@ internal static class PlayerActionSystem { var leaks = level.Leaks.ToArray(); leaks[leakIndex] = leak with { Repaired = true }; - return level.SetUnderground(leak.UndergroundPosition, leak.Carrier, level.GetUnderground(leak.UndergroundPosition, leak.Carrier) with { State = EUndergroundState.Intact }) with { Leaks = leaks }; + return level.SetUnderground(leak.UndergroundPosition, leak.Carrier, level.GetUnderground(leak.UndergroundPosition, leak.Carrier) with { + State = EUndergroundState.Intact, + StructuralIntegrity = Balancing.Current.MaxStructuralIntegrity + }) with { + Leaks = leaks + }; } private static LevelState ApplyElementRemedy(LevelState level, LeakState leak) @@ -121,9 +121,9 @@ internal static class PlayerActionSystem return level.SetProp(position, prop with { JunctionMode = (prop.JunctionMode + 1) % ratios.Count }); } - private static bool CanSpendAction(LevelState level) + private static bool CanAct(LevelState level) { - return level.Global.LevelState is not (ELevelState.Lost or ELevelState.Won) && level.Global.ActionsRemaining > 0; + return level.Global.LevelState is not (ELevelState.Lost or ELevelState.Won); } private static LevelState Refuse(LevelState level, string message) diff --git a/src/ReactorMaintenance.Simulation/Systems/ReactorSystem.cs b/src/ReactorMaintenance.Simulation/Systems/ReactorSystem.cs index e8c2ea5..167a241 100644 --- a/src/ReactorMaintenance.Simulation/Systems/ReactorSystem.cs +++ b/src/ReactorMaintenance.Simulation/Systems/ReactorSystem.cs @@ -34,41 +34,54 @@ internal static class ReactorSystem return level with { Reactors = reactors, Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = "REACTOR HEAT TERMINAL" } }; var hasCritical = level.Surface.Any(surface => SimulationBands.Fuel(surface.Fuel) == EBand.Critical || SimulationBands.Coolant(surface.Coolant) == EBand.Critical || SimulationBands.Electricity(surface.Electricity) == EBand.Critical || SimulationBands.Heat(surface.Heat) == EBand.Critical); - var hasCaution = hasCritical || level.Props.Any(prop => prop.ServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled) || level.Leaks.Any(leak => !leak.Repaired); + var hasCaution = hasCritical + || !HasRequiredConsumers(level) + || level.Props.Any(prop => prop.Type == EPropType.Consumer && HasConsumerTrouble(prop)) + || level.Leaks.Any(leak => !leak.Repaired); var state = hasCritical ? ELevelState.Critical : hasCaution ? ELevelState.Caution : ELevelState.Stable; return level with { Reactors = reactors, Global = level.Global with { LevelState = state, Status = state.ToString().ToUpperInvariant() } }; } - public static bool MatchesReady(LevelState level, RulePredicate predicate) + private static bool IsReady(LevelState level, ReactorState reactor) { - return level.Reactors.Any(reactor => MatchesId(reactor, predicate.ReactorId) && reactor.Ready) == predicate.BoolValue; - } - - public static bool MatchesWon(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 static bool IsReady(LevelState level, ReactorBinding reactor) - { - return HasProducingConsumer(level, reactor.FuelConsumerPosition, ECarrierType.Fuel) - && HasProducingConsumer(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant) - && HasProducingConsumer(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity) + return ReactorFeedsPresentAndProducing(level, reactor.ControlPosition) + && ProducingConsumerCount(level, ECarrierType.Fuel) >= level.RequiredFuelConsumers + && ProducingConsumerCount(level, ECarrierType.Coolant) >= level.RequiredCoolantConsumers + && ProducingConsumerCount(level, ECarrierType.Electricity) >= level.RequiredElectricityConsumers && level.GetSurface(reactor.ControlPosition).Heat < Balancing.Current.TerminalHeat; } - private static bool HasProducingConsumer(LevelState level, GridPosition position, ECarrierType carrier) + private static bool ReactorFeedsPresentAndProducing(LevelState level, GridPosition position) { - return level.InBounds(position) && level.GetProp(position) is { Type: EPropType.Consumer, Carrier: var consumerCarrier, ServiceState: EConsumerServiceState.Producing } && consumerCarrier == carrier; + foreach (var carrier in Enum.GetValues()) + { + var underground = level.GetUnderground(position, carrier); + if (underground.IsPresent && (underground.Amount <= 0 || underground.Intensity <= 0)) + return false; + } + + return true; } - private static bool MatchesId(ReactorBinding reactor, int reactorId) + private static int ProducingConsumerCount(LevelState level, ECarrierType carrier) { - return reactorId <= 0 || reactor.ReactorId == reactorId; + return LevelTraversal.AllPositions(level) + .Count(position => level.GetProp(position) is { Type: EPropType.Consumer } prop && prop.ServiceStateFor(carrier) == EConsumerServiceState.Producing); + } + + private static bool HasRequiredConsumers(LevelState level) + { + return ProducingConsumerCount(level, ECarrierType.Fuel) >= level.RequiredFuelConsumers + && ProducingConsumerCount(level, ECarrierType.Coolant) >= level.RequiredCoolantConsumers + && ProducingConsumerCount(level, ECarrierType.Electricity) >= level.RequiredElectricityConsumers; + } + + private static bool HasConsumerTrouble(PropState prop) + { + return prop.FuelServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled + || prop.CoolantServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled + || prop.ElectricityServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled; } private static LevelState Refuse(LevelState level, string message) diff --git a/src/ReactorMaintenance.Simulation/Systems/RuleEventSystem.cs b/src/ReactorMaintenance.Simulation/Systems/RuleEventSystem.cs deleted file mode 100644 index 6b108c4..0000000 --- a/src/ReactorMaintenance.Simulation/Systems/RuleEventSystem.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -internal static class RuleEventSystem -{ - public static LevelState Apply(LevelState level, ERuleEventPhase phase) - { - var next = level; - var events = level.RuleEvents.Select((ruleEvent, index) => (Event: ruleEvent, Index: index)).Where(item => item.Event.Enabled && item.Event.Phase == phase && (item.Event.Repeat || !item.Event.Triggered)).OrderBy(item => item.Event.Priority).ToArray(); - var ruleEvents = next.RuleEvents.ToArray(); - - foreach (var item in events) - { - if (!item.Event.Predicates.All(predicate => PredicateMatches(next, predicate))) - continue; - - foreach (var effect in item.Event.Effects) - next = ApplyEffect(next, effect); - - ruleEvents[item.Index] = item.Event with { Triggered = true }; - } - - return next with { RuleEvents = ruleEvents }; - } - - public static bool PredicateMatches(LevelState level, RulePredicate predicate) - { - return predicate.Kind switch { - ERulePredicateKind.TurnAtLeast => level.Global.Turn >= predicate.Turn, - ERulePredicateKind.LevelStateIs => level.Global.LevelState == predicate.LevelState, - ERulePredicateKind.ReactorReadyIs => ReactorSystem.MatchesReady(level, predicate), - ERulePredicateKind.ReactorLostIs => level.Global.LevelState == ELevelState.Lost == predicate.BoolValue, - ERulePredicateKind.ReactorWonIs => ReactorSystem.MatchesWon(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) && SimulationBands.NetworkBand(level.GetUnderground(predicate.Position, predicate.Carrier), predicate.Carrier, predicate.NetworkValue) >= predicate.Band, - ERulePredicateKind.SurfaceBandAt => level.InBounds(predicate.Position) && SimulationBands.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 - }; - } - - private static LevelState ApplyEffect(LevelState level, RuleEffect effect) - { - return effect.Kind switch { - ERuleEffectKind.StartLeak => StartLeak(level, effect), - ERuleEffectKind.WorsenLeak => level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Leaking }), - ERuleEffectKind.RepairNetworkCell => level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Intact }), - 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, SurfaceCarrierMath.AddCarrier(level.GetSurface(effect.Position), effect.Carrier, effect.Amount)), - ERuleEffectKind.RemoveSurfaceHazard => level.SetSurface(effect.Position, SurfaceCarrierMath.AddCarrier(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 - }; - } - - private static LevelState StartLeak(LevelState level, RuleEffect effect) - { - var leak = new LeakState { - Carrier = effect.Carrier, - UndergroundPosition = 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] }; - } -} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Systems/StructuralIntegritySystem.cs b/src/ReactorMaintenance.Simulation/Systems/StructuralIntegritySystem.cs new file mode 100644 index 0000000..444929f --- /dev/null +++ b/src/ReactorMaintenance.Simulation/Systems/StructuralIntegritySystem.cs @@ -0,0 +1,82 @@ +namespace ReactorMaintenance.Simulation; + +internal static class StructuralIntegritySystem +{ + public static LevelState Resolve(LevelState level) + { + foreach (var carrier in Enum.GetValues()) + level = ResolveCarrier(level, carrier); + + return level; + } + + private static LevelState ResolveCarrier(LevelState level, ECarrierType carrier) + { + var layer = level.Layer(carrier).ToArray(); + var leaks = level.Leaks.ToList(); + + foreach (var position in LevelTraversal.AllPositions(level)) + { + var index = level.Index(position); + var cell = layer[index]; + if (!cell.IsPresent) + continue; + + var integrity = DegradeIntegrity(cell, carrier); + var state = cell.State; + if (state != EUndergroundState.Leaking && integrity <= Balancing.Current.StructuralIntegrityLeakThreshold && cell.Intensity > 0) + { + state = EUndergroundState.Leaking; + leaks = UpsertLeak(leaks, level, position, carrier); + } + + layer[index] = cell with { State = state, StructuralIntegrity = integrity }; + } + + var next = carrier switch { + ECarrierType.Fuel => level with { Fuel = layer }, + ECarrierType.Coolant => level with { Coolant = layer }, + ECarrierType.Electricity => level with { Electricity = layer }, + _ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.") + }; + + return next with { Leaks = leaks.ToArray() }; + } + + private static int DegradeIntegrity(UndergroundCell cell, ECarrierType carrier) + { + if (cell.StructuralIntegrity >= Balancing.Current.MaxStructuralIntegrity && cell.Intensity <= Balancing.Current.StructuralPressureThreshold(carrier)) + return Balancing.Current.MaxStructuralIntegrity; + + var overPressure = Math.Max(0, cell.Intensity - Balancing.Current.StructuralPressureThreshold(carrier)); + if (overPressure <= 0 || cell.StructuralIntegrity >= Balancing.Current.MaxStructuralIntegrity) + return Math.Clamp(cell.StructuralIntegrity, 0, Balancing.Current.MaxStructuralIntegrity); + + var damage = Math.Max(1, (int)Math.Ceiling(overPressure * Balancing.Current.StructuralIntegrityDamageScale)); + return Math.Clamp(cell.StructuralIntegrity - damage, 0, Balancing.Current.MaxStructuralIntegrity); + } + + private static List UpsertLeak(List leaks, LevelState level, GridPosition position, ECarrierType carrier) + { + var accessPosition = carrier == ECarrierType.Electricity + ? position.Neighbors().FirstOrDefault(level.IsFloor) + : position; + if (accessPosition is null || !level.IsFloor(accessPosition)) + return leaks; + + var index = leaks.FindIndex(leak => leak.UndergroundPosition == position && leak.Carrier == carrier); + var leakState = new LeakState { + Carrier = carrier, + UndergroundPosition = position, + AccessPosition = accessPosition, + Repaired = false + }; + + if (index >= 0) + leaks[index] = leakState; + else + leaks.Add(leakState); + + return leaks; + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs index a20227f..97badc2 100644 --- a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs +++ b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs @@ -280,7 +280,6 @@ public sealed partial class MainWindow ClearPendingEditorOperation(); m_Level = m_Level.SetProp(position, new()).SetSurface(position, new()) with { Leaks = m_Level.Leaks.Where(leak => leak.AccessPosition != position && leak.UndergroundPosition != position).ToArray(), - Doors = m_Level.Doors.Where(door => door.A != position && door.B != position).ToArray(), Reactors = m_Level.Reactors.Where(reactor => reactor.ControlPosition != position).ToArray() }; m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) }; @@ -384,6 +383,11 @@ public sealed partial class MainWindow return m_Level.InBounds(position) ? m_Level.GetTerrain(position) : ECellTerrain.Wall; } + private bool IsWall(GridPosition position) + { + return m_Level.InBounds(position) && m_Level.GetTerrain(position) == ECellTerrain.Wall; + } + private void DrawUnderground(CanvasDrawingSession drawing, CanvasLayout layout) { foreach (var position in AllPositions()) @@ -431,11 +435,19 @@ public sealed partial class MainWindow private void DrawDoors(CanvasDrawingSession drawing, CanvasLayout layout) { - foreach (var door in m_Level.Doors) + foreach (var position in AllPositions()) { - var centerA = Center(layout.CellRect(door.A)); - var centerB = Center(layout.CellRect(door.B)); - drawing.DrawLine((float)centerA.X, (float)centerA.Y, (float)centerB.X, (float)centerB.Y, door.State == EDoorState.Open ? Colors.LightGreen : Colors.OrangeRed, 5); + var prop = m_Level.GetProp(position); + if (prop.Type != EPropType.Door) + continue; + + var rect = layout.CellRect(position); + var center = Center(rect); + var color = prop.DoorState == EDoorState.Open ? Colors.LightGreen : Colors.OrangeRed; + if (IsWall(new(position.X, position.Y - 1)) && IsWall(new(position.X, position.Y + 1))) + drawing.DrawLine((float)rect.Left, (float)center.Y, (float)rect.Right, (float)center.Y, color, 5); + else + drawing.DrawLine((float)center.X, (float)rect.Top, (float)center.X, (float)rect.Bottom, color, 5); } } @@ -554,11 +566,11 @@ public sealed partial class MainWindow LevelNameText.Text = m_Level.Name; TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture); StatusText.Text = $"{m_Level.Global.LevelState}: {m_Level.Global.Status}"; - GlobalText.Text = $"Actions: {m_Level.Global.ActionsRemaining}/{Balancing.Current.ActionsPerTurn}\n" - + $"All-seeing-eye: {(m_Level.Global.AllSeeingEyeUnlocked ? "unlocked" : "locked")}\n" - + $"Inventory: F {m_Level.Robot.FuelNeutralizers}, C {m_Level.Robot.CoolantNeutralizers}, E {m_Level.Robot.ElectricityNeutralizers}, H {m_Level.Robot.HeatShields}\n" + var doorCount = m_Level.Props.Count(prop => prop.Type == EPropType.Door); + GlobalText.Text = $"Inventory: F {m_Level.Robot.FuelNeutralizers}, C {m_Level.Robot.CoolantNeutralizers}, E {m_Level.Robot.ElectricityNeutralizers}, H {m_Level.Robot.HeatShields}\n" + $"Heat immunity steps: {m_Level.Robot.HeatImmunitySteps}\n" - + $"Reactors: {m_Level.Reactors.Count}, Leaks: {m_Level.Leaks.Count}, Doors: {m_Level.Doors.Count}"; + + $"Required consumers F/C/E: {m_Level.RequiredFuelConsumers}/{m_Level.RequiredCoolantConsumers}/{m_Level.RequiredElectricityConsumers}\n" + + $"Reactors: {m_Level.Reactors.Count}, Leaks: {m_Level.Leaks.Count}, Doors: {doorCount}"; CellText.Text = m_SelectedCell is { } position && m_Level.InBounds(position) ? CellInspectionText(position) : "No cell selected."; ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel($"{forecast.Turns}: {forecast.Message}")).ToArray(); @@ -588,19 +600,8 @@ public sealed partial class MainWindow private void ApplyDoorTool(GridPosition position) { - if (!m_Level.IsFloor(position)) - return; - - if (m_PendingDoorCell is not { } pending) - { - m_PendingDoorCell = position; - m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool); - RefreshForecasts(); - return; - } - - m_Level = LevelEditor.SetDoorEdge(m_Level, pending, position); - m_PendingDoorCell = null; + ClearPendingEditorOperation(); + m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool); RefreshForecasts(); } @@ -641,42 +642,17 @@ public sealed partial class MainWindow private void AddWarningRule_Click(object sender, RoutedEventArgs e) { - var turn = m_Level.Global.Turn + 1; - m_Level = LevelEditor.AddRuleEvent(m_Level, new() { - Phase = ERuleEventPhase.EndOfTurn, - ForecastText = $"Warning on turn {turn}", - Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = turn }], - Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = $"Authored warning on turn {turn}" }] - }); - RefreshForecasts(); - RefreshInspector(); + StatusText.Text = "Rule events were removed from level authoring."; } private void AddLeakRule_Click(object sender, RoutedEventArgs e) { - if (m_SelectedCell is not { } position || !TryGetAuthoredLeakCarrier(position, out var carrier)) - return; - - var turn = m_Level.Global.Turn + 1; - m_Level = LevelEditor.AddRuleEvent(m_Level, new() { - Phase = ERuleEventPhase.EndOfTurn, - ForecastText = $"{carrier} leak starts on turn {turn}", - Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = turn }], - Effects = [new() { Kind = ERuleEffectKind.StartLeak, Position = position, AccessPosition = position, Carrier = carrier }] - }); - RefreshForecasts(); - RefreshInspector(); + StatusText.Text = "Rule events were removed from level authoring."; } private void RemoveLastRule_Click(object sender, RoutedEventArgs e) { - var ruleEvent = m_Level.RuleEvents.LastOrDefault(); - if (ruleEvent is null) - return; - - m_Level = LevelEditor.RemoveRuleEvent(m_Level, ruleEvent.Id); - RefreshForecasts(); - RefreshInspector(); + StatusText.Text = "Rule events were removed from level authoring."; } private void BindSelectedConsumer(ECarrierType carrier) @@ -684,9 +660,7 @@ public sealed partial class MainWindow if (m_SelectedCell is not { } position || m_SelectedReactorId is not { } reactorId) return; - m_Level = LevelEditor.BindReactorConsumer(m_Level, reactorId, carrier, position); - RefreshForecasts(); - RefreshInspector(); + StatusText.Text = "Reactors now use required consumer counts instead of bindings."; } private string CellInspectionText(GridPosition position) @@ -698,7 +672,7 @@ public sealed partial class MainWindow var electricity = m_Level.GetUnderground(position, ECarrierType.Electricity); return $"Position: {position.X},{position.Y}\n" + $"Terrain: {m_Level.GetTerrain(position)}\n" - + $"Prop: {prop.Type} {prop.Carrier} {prop.SwitchState} {prop.ServiceState}\n" + + $"Prop: {prop.Type} {prop.SwitchState} {prop.ServiceState}\n" + $"Fuel: {UndergroundText(fuel)}\n" + $"Coolant: {UndergroundText(coolant)}\n" + $"Electricity: {UndergroundText(electricity)}\n" @@ -708,9 +682,6 @@ public sealed partial class MainWindow private string WorkflowInspectionText() { - if (m_PendingDoorCell is { } door) - return $"Door edge: select an adjacent floor for {door.X},{door.Y}."; - if (m_PendingElectricityLeakCell is { } leak) return $"Electricity leak face: select adjacent floor access for {leak.X},{leak.Y}."; @@ -724,18 +695,14 @@ public sealed partial class MainWindow return "Select or place a reactor control."; return $"Reactor {reactor.ReactorId}\n" - + $"Fuel: {PositionText(reactor.FuelConsumerPosition)}\n" - + $"Coolant: {PositionText(reactor.CoolantConsumerPosition)}\n" - + $"Electric: {PositionText(reactor.ElectricityConsumerPosition)}"; + + $"Control: {PositionText(reactor.ControlPosition)}\n" + + $"Ready: {reactor.Ready}\n" + + $"Required F/C/E: {m_Level.RequiredFuelConsumers}/{m_Level.RequiredCoolantConsumers}/{m_Level.RequiredElectricityConsumers}"; } private string RuleEventInspectionText() { - if (m_Level.RuleEvents.Count == 0) - return "No authored rule events."; - - var last = m_Level.RuleEvents[^1]; - return $"{m_Level.RuleEvents.Count} authored. Last: {last.Id} ({last.Phase})."; + return "Rule events were removed from level authoring."; } private static string PositionText(GridPosition position) @@ -745,7 +712,7 @@ public sealed partial class MainWindow private static string UndergroundText(UndergroundCell cell) { - return $"{cell.State} amount {Format(cell.Amount)} intensity {Format(cell.Intensity)}"; + return $"{cell.State} amount {Format(cell.Amount)} intensity {Format(cell.Intensity)} integrity {cell.StructuralIntegrity}"; } private static string Format(float value) @@ -762,26 +729,25 @@ public sealed partial class MainWindow level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel }); level = level.SetProp(new(2, 5), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant }); level = level.SetProp(new(2, 7), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity }); - level = level.SetProp(new(5, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel }); - level = level.SetProp(new(5, 5), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant }); - level = level.SetProp(new(5, 7), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Electricity }); + level = level.SetProp(new(5, 3), new() { Type = EPropType.Consumer }); + level = level.SetProp(new(5, 5), new() { Type = EPropType.Consumer }); + level = level.SetProp(new(5, 7), new() { Type = EPropType.Consumer }); level = level.SetProp(new(10, 5), new() { Type = EPropType.ReactorControl, ReactorId = 1 }); level = level.SetProp(new(11, 4), new() { Type = EPropType.AllSeeingEyeTerminal }); level = level.SetProp(new(11, 6), new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.HeatShield }); level = level.SetUnderground(new(4, 5), ECarrierType.Coolant, new() { State = EUndergroundState.Leaking }) with { Leaks = [new() { Carrier = ECarrierType.Coolant, UndergroundPosition = new(4, 5), AccessPosition = new(4, 5) }], - Doors = [new() { A = new(8, 5), B = new(9, 5), State = EDoorState.Closed }], Robot = new() { Position = new(10, 5) }, Reactors = [ new() { ReactorId = 1, - ControlPosition = new(10, 5), - FuelConsumerPosition = new(5, 3), - CoolantConsumerPosition = new(5, 5), - ElectricityConsumerPosition = new(5, 7) + ControlPosition = new(10, 5) } ] }; + level = level.SetTerrain(new(8, 4), ECellTerrain.Wall); + level = level.SetTerrain(new(8, 6), ECellTerrain.Wall); + level = level.SetProp(new(8, 5), new() { Type = EPropType.Door }); return level with { Forecasts = new SimulationEngine().Forecast(level) }; } @@ -860,7 +826,7 @@ public sealed partial class MainWindow { return prop.Type switch { EPropType.Flow => $"{CarrierShort(prop.Carrier)} SRC", - EPropType.Consumer => $"{CarrierShort(prop.Carrier)} CON", + EPropType.Consumer => "CON", EPropType.Junction => $"J {prop.JunctionMode}", EPropType.Door => "DOOR", EPropType.AllSeeingEyeTerminal => "EYE", @@ -975,7 +941,6 @@ public sealed partial class MainWindow private void ClearPendingEditorOperation() { - m_PendingDoorCell = null; m_PendingElectricityLeakCell = null; } @@ -1005,7 +970,6 @@ public sealed partial class MainWindow private LevelState m_Level; private double m_PanX; private double m_PanY; - private GridPosition? m_PendingDoorCell; private GridPosition? m_PendingElectricityLeakCell; private CanvasBitmap? m_RobotSprite; private GridPosition? m_SelectedCell; diff --git a/tests/ReactorMaintenance.Simulation.Tests/LevelEditorTests.cs b/tests/ReactorMaintenance.Simulation.Tests/LevelEditorTests.cs index 5de1c31..c5baffb 100644 --- a/tests/ReactorMaintenance.Simulation.Tests/LevelEditorTests.cs +++ b/tests/ReactorMaintenance.Simulation.Tests/LevelEditorTests.cs @@ -3,18 +3,27 @@ public sealed class LevelEditorTests { [Fact] - public void DoorToolRequiresExplicitAdjacentEdgeSelection() + public void DoorToolPlacesSingleFloorDoorProp() { var level = LevelState.Create("Door editor", 6, 6); - var withDoorProp = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Door }); - var withDoorEdge = LevelEditor.SetDoorEdge(withDoorProp, new(2, 2), new(3, 2)); - var rejected = LevelEditor.SetDoorEdge(withDoorEdge, new(2, 2), new(4, 2)); + var next = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Door }); - Assert.Equal(EPropType.Door, withDoorProp.GetProp(new(2, 2)).Type); - Assert.Empty(withDoorProp.Doors); - Assert.Single(withDoorEdge.Doors); - Assert.Equal(withDoorEdge.Doors, rejected.Doors); + Assert.Equal(EPropType.Door, next.GetProp(new(2, 2)).Type); + Assert.Equal(EDoorState.Closed, next.GetProp(new(2, 2)).DoorState); + } + + [Fact] + public void ConsumerToolPlacesCarrierAgnosticConsumer() + { + var level = LevelState.Create("Consumer editor", 6, 6); + + var next = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Consumer, Carrier = ECarrierType.Fuel }); + + Assert.Equal(EPropType.Consumer, next.GetProp(new(2, 2)).Type); + Assert.Equal(EConsumerServiceState.Unknown, next.GetProp(new(2, 2)).FuelServiceState); + Assert.Equal(EConsumerServiceState.Unknown, next.GetProp(new(2, 2)).CoolantServiceState); + Assert.Equal(EConsumerServiceState.Unknown, next.GetProp(new(2, 2)).ElectricityServiceState); } [Fact] @@ -33,44 +42,14 @@ public sealed class LevelEditorTests } [Fact] - public void ReactorBindingUpdatesOnlyMatchingCarrierConsumer() + public void ReactorControlToolCreatesUnboundReactorState() { - var level = LevelState.Create("Binding editor", 8, 6); - level = level.SetProp(new(1, 1), new() { Type = EPropType.ReactorControl, ReactorId = 1 }); - level = level.SetProp(new(2, 1), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel }); - level = level.SetProp(new(3, 1), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant }) with { - Reactors = [ - new() { - ReactorId = 1, - ControlPosition = new(1, 1), - FuelConsumerPosition = new(1, 1), - CoolantConsumerPosition = new(1, 1), - ElectricityConsumerPosition = new(1, 1) - } - ] - }; + var level = LevelState.Create("Reactor editor", 6, 6); - var bound = LevelEditor.BindReactorConsumer(level, 1, ECarrierType.Fuel, new(2, 1)); - var rejected = LevelEditor.BindReactorConsumer(bound, 1, ECarrierType.Fuel, new(3, 1)); + var next = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.ReactorControl }); - Assert.Equal(new(2, 1), bound.Reactors[0].FuelConsumerPosition); - Assert.Equal(bound.Reactors[0].FuelConsumerPosition, rejected.Reactors[0].FuelConsumerPosition); - } - - [Fact] - public void RuleEventEditorAssignsStableIdsAndCanRemoveEvents() - { - var level = LevelState.Create("Rule editor", 6, 6); - - var withRule = LevelEditor.AddRuleEvent(level, new() { - Phase = ERuleEventPhase.EndOfTurn, - Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 1 }], - Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = "authored" }] - }); - var removed = LevelEditor.RemoveRuleEvent(withRule, "rule-1"); - - Assert.Single(withRule.RuleEvents); - Assert.Equal("rule-1", withRule.RuleEvents[0].Id); - Assert.Empty(removed.RuleEvents); + Assert.Single(next.Reactors); + Assert.Equal(new(2, 2), next.Reactors[0].ControlPosition); + Assert.Equal(1, next.Reactors[0].ReactorId); } } \ No newline at end of file diff --git a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs index f269f1a..4b34613 100644 --- a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs +++ b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs @@ -3,19 +3,31 @@ public sealed class SimulationEngineTests { [Fact] - public void NetworkPropagationSuppliesBoundConsumersAndReadiesReactor() + public void NetworkPropagationSuppliesConsumerServicesAndReadiesReactor() { var level = BuildReadyLevel(); var next = m_Engine.AdvanceTurn(level); + var consumer = next.GetProp(new(3, 3)); - Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 2)).ServiceState); - Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 3)).ServiceState); - Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 4)).ServiceState); + Assert.Equal(EConsumerServiceState.Producing, consumer.FuelServiceState); + Assert.Equal(EConsumerServiceState.Producing, consumer.CoolantServiceState); + Assert.Equal(EConsumerServiceState.Producing, consumer.ElectricityServiceState); Assert.Equal(ELevelState.Ready, next.Global.LevelState); Assert.Contains(next.Forecasts, forecast => forecast.Kind == EForecastKind.ReactorReady); } + [Fact] + public void ReactorNeedsPositiveFlowOnlyForNetworksBeneathControl() + { + var level = BuildReadyLevel(); + level = level.SetUnderground(new(5, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact }); + + var next = m_Engine.AdvanceTurn(level); + + Assert.NotEqual(ELevelState.Ready, next.Global.LevelState); + } + [Fact] public void ReactorActivatesOnlyAtReadyControl() { @@ -30,19 +42,110 @@ public sealed class SimulationEngineTests } [Fact] - public void LeakingUndergroundCellInjectsMatchingSurfaceHazard() + public void DisabledConsumerReportsDisabledOnlyForNetworksBeneathIt() { - var level = LevelState.Create("Leak", 6, 6); - level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Leaking, Amount = 5, Intensity = 5 }) with { - Leaks = [new() { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }] + var level = LevelState.Create("Disabled", 6, 6); + level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Intact }); + level = level.SetProp(new(2, 2), new() { Type = EPropType.Consumer, SwitchState = EPropSwitchState.Disabled }); + + var next = m_Engine.AdvanceTurn(level); + var consumer = next.GetProp(new(2, 2)); + + Assert.Equal(EConsumerServiceState.Disabled, consumer.FuelServiceState); + Assert.Equal(EConsumerServiceState.Unknown, consumer.CoolantServiceState); + Assert.Equal(EConsumerServiceState.Unknown, consumer.ElectricityServiceState); + Assert.Equal(EConsumerServiceState.Disabled, consumer.ServiceState); + } + + [Fact] + public void MovementIsQuickAndDoesNotResolveSimulationStep() + { + var level = LevelState.Create("Quick", 6, 6) with { + Robot = new() { Position = new(1, 1) } }; - level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel }); + + var next = m_Engine.MoveRobot(level, new(2, 1)); + + Assert.Equal(new(2, 1), next.Robot.Position); + Assert.Equal(0, next.Global.Turn); + } + + [Fact] + public void DoorInteractionIsLengthyAndResolvesSimulationStep() + { + var level = DoorLevel(); + level = level with { Robot = new() { Position = new(3, 2) } }; + + var next = m_Engine.InteractProp(level); + + Assert.Equal(EDoorState.Open, next.GetProp(new(3, 2)).DoorState); + Assert.Equal(1, next.Global.Turn); + } + + [Fact] + public void ClosedInferredDoorBlocksAdjacentHeatFlow() + { + var level = DoorLevel(); + level = level.SetSurface(new(3, 2), new() { Heat = 8 }); var next = m_Engine.AdvanceTurn(level); + Assert.Equal(0, next.GetSurface(new(4, 2)).Heat); + } + + [Fact] + public void StructuralIntegrityCreatesLeakWhenWeakCellHasPositivePressure() + { + var level = LevelState.Create("Integrity leak", 6, 6); + level = AddLine(level, ECarrierType.Fuel, new(1, 2), new(2, 2)); + level = level.SetProp(new(1, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel }); + level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, level.GetUnderground(new(2, 2), ECarrierType.Fuel) with { + StructuralIntegrity = Balancing.Current.StructuralIntegrityLeakThreshold + }); + + var next = m_Engine.AdvanceTurn(level); + + Assert.Equal(EUndergroundState.Leaking, next.GetUnderground(new(2, 2), ECarrierType.Fuel).State); + Assert.Contains(next.Leaks, leak => leak.Carrier == ECarrierType.Fuel && leak.UndergroundPosition == new GridPosition(2, 2)); Assert.True(next.GetSurface(new(2, 2)).Fuel > 0); } + [Fact] + public void HighPressureWorsensNonMaxStructuralIntegrity() + { + var level = LevelState.Create("Integrity damage", 6, 6); + level = AddLine(level, ECarrierType.Fuel, new(1, 2), new(2, 2)); + level = level.SetProp(new(1, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel }); + level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, level.GetUnderground(new(2, 2), ECarrierType.Fuel) with { + StructuralIntegrity = Balancing.Current.MaxStructuralIntegrity - 1 + }); + + var next = m_Engine.AdvanceTurn(level); + + Assert.True(next.GetUnderground(new(2, 2), ECarrierType.Fuel).StructuralIntegrity < Balancing.Current.MaxStructuralIntegrity - 1); + } + + [Fact] + public void RepairingLeakRestoresStructuralIntegrity() + { + var level = LevelState.Create("Repair", 6, 6); + level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { + State = EUndergroundState.Leaking, + Amount = 5, + Intensity = 5, + StructuralIntegrity = 0 + }) with { + Robot = new() { Position = new(2, 2) }, + Leaks = [new() { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }] + }; + + var next = m_Engine.InteractLeak(level, ECarrierType.Fuel, false); + + Assert.True(next.Leaks[0].Repaired); + Assert.Equal(EUndergroundState.Intact, next.GetUnderground(new(2, 2), ECarrierType.Fuel).State); + Assert.Equal(Balancing.Current.MaxStructuralIntegrity, next.GetUnderground(new(2, 2), ECarrierType.Fuel).StructuralIntegrity); + } + [Fact] public void ElementRemedyClearsHazardAndBlocksImmediateReentry() { @@ -60,19 +163,6 @@ public sealed class SimulationEngineTests Assert.Equal(0, next.Robot.FuelNeutralizers); } - [Fact] - public void ClosedDoorBlocksAdjacentHeatFlow() - { - var level = LevelState.Create("Door", 6, 6); - level = level.SetSurface(new(2, 2), new() { Heat = 8 }) with { - Doors = [new() { A = new(2, 2), B = new(3, 2), State = EDoorState.Closed }] - }; - - var next = m_Engine.AdvanceTurn(level); - - Assert.Equal(0, next.GetSurface(new(3, 2)).Heat); - } - [Fact] public void HeatShieldPreventsRobotHeatLoss() { @@ -122,217 +212,19 @@ public sealed class SimulationEngineTests } [Fact] - public void ValidatorRejectsJunctionWithoutTwoOrThreeOutflows() - { - var level = BuildJunctionLevel(2); - level = level.SetUnderground(new(3, 3), ECarrierType.Fuel, new()); - - var report = new LevelValidator().Validate(level); - - Assert.False(report.IsValid); - Assert.Contains(report.Errors, error => error.Message.Contains("one incoming branch and two or three outgoing branches", StringComparison.Ordinal)); - } - - [Fact] - public void NonJunctionCellsAcceptBestFlowFromMultipleSourcePaths() - { - var level = LevelState.Create("Best path", 7, 7); - level = AddHorizontalUnderground(level, ECarrierType.Fuel, 1, 5, 3); - level = level.SetProp(new(1, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel }); - level = level.SetProp(new(5, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel }); - level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel }); - - var report = new LevelValidator().Validate(level); - var next = m_Engine.AdvanceTurn(level); - - Assert.True(report.IsValid); - Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 3)).ServiceState); - } - - [Fact] - public void RobotLosesOnUnsafeElementHazard() - { - var level = LevelState.Create("Unsafe", 6, 6); - level = level.SetSurface(new(2, 2), new() { Electricity = Balancing.Current.MaxValue }) with { - Robot = new() { Position = new(2, 2) } - }; - - var next = m_Engine.AdvanceTurn(level); - - Assert.Equal(ELevelState.Lost, next.Global.LevelState); - } - - [Fact] - public void RuleEventCanCreateTerminalLossForecast() - { - var level = LevelState.Create("Rule", 6, 6) with { - RuleEvents = [ - new() { - Phase = ERuleEventPhase.EndOfTurn, - ForecastText = "containment failure", - Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }], - Effects = [new() { Kind = ERuleEffectKind.MarkTerminalLoss, Message = "CONTAINMENT FAILURE" }] - } - ] - }; - - var forecasts = m_Engine.Forecast(level); - - Assert.Contains(forecasts, forecast => forecast.Kind == EForecastKind.RuleEvent && forecast.Message == "containment failure"); - 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() { - Phase = ERuleEventPhase.EndOfTurn, - Predicates = [new() { Kind = ERulePredicateKind.NetworkBandAt, Position = new(3, 2), Carrier = ECarrierType.Fuel, NetworkValue = ENetworkValueKind.Amount, Band = EBand.Critical }], - Effects = [new() { 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() { - Phase = ERuleEventPhase.EndOfTurn, - Predicates = [new() { Kind = ERulePredicateKind.ReactorReadyIs, ReactorId = 1, BoolValue = true }], - Effects = [new() { 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() { - Phase = ERuleEventPhase.StartOfSimulation, - Predicates = [new() { Kind = ERulePredicateKind.RobotInventoryAtLeast, Remedy = ERemedyType.FuelNeutralizer, InventoryCount = 1 }], - Effects = [new() { 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() { A = new(2, 2), B = new(1, 2), State = EDoorState.Closed }, - new() { A = new(2, 2), B = new(3, 2), State = EDoorState.Closed }, - new() { A = new(2, 2), B = new(2, 1), State = EDoorState.Closed }, - new() { A = new(2, 2), B = new(2, 3), State = EDoorState.Closed } - ], - RuleEvents = [ - new() { - Phase = ERuleEventPhase.StartOfSimulation, - Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }], - Effects = [ - new() { Kind = ERuleEffectKind.RemoveSurfaceHazard, Position = new(2, 2), Carrier = ECarrierType.Fuel, Amount = 2 }, - new() { Kind = ERuleEffectKind.RemoveHeat, Position = new(2, 2), Amount = 3 }, - new() { 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() { - Phase = ERuleEventPhase.StartOfSimulation, - Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }], - Effects = [new() { 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() { - Predicates = [new() { Kind = ERulePredicateKind.PropStateAt, Position = new(1, 1) }], - Effects = [ - new() { Kind = ERuleEffectKind.AddSurfaceHazard, Position = new(2, 2), Carrier = ECarrierType.Fuel, Amount = 1 }, - new() { 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() + public void ValidatorRejectsInvalidDoorGeometryAndWallHazards() { var level = LevelState.Create("Invalid", 6, 6); - level = level.SetTerrain(new(2, 2), ECellTerrain.Wall); - level = level with { - Surface = level.Surface.ToArray(), - Reactors = [new() { ControlPosition = new(3, 3), FuelConsumerPosition = new(1, 1), CoolantConsumerPosition = new(1, 1), ElectricityConsumerPosition = new(1, 1) }] - }; - level.Surface[level.Index(new(2, 2))] = new() { Heat = 1 }; + level = level.SetProp(new(2, 2), new() { Type = EPropType.Door }); + level = level.SetTerrain(new(4, 4), ECellTerrain.Wall); + level = level with { Surface = level.Surface.ToArray() }; + level.Surface[level.Index(new(4, 4))] = new() { Heat = 1 }; var report = new LevelValidator().Validate(level); Assert.False(report.IsValid); + Assert.Contains(report.Errors, error => error.Message.Contains("Door must be surrounded", StringComparison.Ordinal)); Assert.Contains(report.Errors, error => error.Message.Contains("Wall cell", StringComparison.Ordinal)); - Assert.Contains(report.Errors, error => error.Message.Contains("Reactor binding", StringComparison.Ordinal)); } [Fact] @@ -343,36 +235,27 @@ public sealed class SimulationEngineTests var json = LevelSerializer.Serialize(level); var loaded = LevelSerializer.Deserialize(json); - Assert.Contains("\"Version\": 2", json); + Assert.Contains("\"Version\": 3", json); Assert.Equal(level.Name, loaded.Name); - Assert.Equal(EPropType.Flow, loaded.GetProp(new(2, 2)).Type); + Assert.Equal(EPropType.Consumer, loaded.GetProp(new(3, 3)).Type); + Assert.Equal(level.RequiredFuelConsumers, loaded.RequiredFuelConsumers); } [Fact] - public void LevelSerializationRoundTripsRuleEventsDoorsAndElectricityLeakFaces() + public void LevelSerializationRoundTripsPropDoorsAndElectricityLeakFaces() { var level = BuildReadyLevel(); level = level.SetTerrain(new(6, 4), ECellTerrain.Wall); level = level.SetUnderground(new(6, 4), ECarrierType.Electricity, new() { State = EUndergroundState.Leaking }) with { - Doors = [new() { A = new(5, 3), B = new(5, 4), State = EDoorState.Closed }], - Leaks = [new() { Carrier = ECarrierType.Electricity, UndergroundPosition = new(6, 4), AccessPosition = new(6, 3) }], - RuleEvents = [ - new() { - Id = "authored", - Phase = ERuleEventPhase.EndOfTurn, - Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 2 }], - Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = "serialized" }] - } - ] + Leaks = [new() { Carrier = ECarrierType.Electricity, UndergroundPosition = new(6, 4), AccessPosition = new(6, 3) }] }; + level = DoorLevel(level); var loaded = LevelSerializer.Deserialize(LevelSerializer.Serialize(level)); - Assert.Single(loaded.Doors); + Assert.Equal(EPropType.Door, loaded.GetProp(new(3, 2)).Type); Assert.Single(loaded.Leaks); - Assert.Single(loaded.RuleEvents); Assert.Equal(new(6, 3), loaded.Leaks[0].AccessPosition); - Assert.Equal("authored", loaded.RuleEvents[0].Id); } [Fact] @@ -380,43 +263,45 @@ public sealed class SimulationEngineTests { var json = """ { - "Version": 1, + "Version": 2, "Level": {} } """; var exception = Assert.Throws(() => LevelSerializer.Deserialize(json)); - Assert.Contains("Unsupported level file version 1", exception.Message); + Assert.Contains("Unsupported level file version 2", exception.Message); } private static LevelState BuildReadyLevel() { var level = LevelState.Create("Ready", 8, 7); - level = AddLine(level, ECarrierType.Fuel, new(2, 2), new(3, 2)); + level = AddLine(level, ECarrierType.Fuel, new(2, 3), new(3, 3)); level = AddLine(level, ECarrierType.Coolant, new(2, 3), new(3, 3)); - level = AddLine(level, ECarrierType.Electricity, new(2, 4), new(3, 4)); - level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel }); - level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant }); + level = AddLine(level, ECarrierType.Electricity, new(2, 3), new(3, 3)); + level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel }); + level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant }); + level = level.SetUnderground(new(2, 2), ECarrierType.Coolant, new() { State = EUndergroundState.Intact }); + level = level.SetUnderground(new(2, 3), ECarrierType.Coolant, new() { State = EUndergroundState.Intact }); level = level.SetProp(new(2, 4), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity }); - level = level.SetProp(new(3, 2), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel }); - level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant }); - level = level.SetProp(new(3, 4), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Electricity }); + level = level.SetUnderground(new(2, 4), ECarrierType.Electricity, new() { State = EUndergroundState.Intact }); + level = level.SetUnderground(new(2, 3), ECarrierType.Electricity, new() { State = EUndergroundState.Intact }); + level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer }); level = level.SetProp(new(5, 3), new() { Type = EPropType.ReactorControl, ReactorId = 1 }); return level with { Robot = new() { Position = new(5, 3) }, - Reactors = [ - new() { - ReactorId = 1, - ControlPosition = new(5, 3), - FuelConsumerPosition = new(3, 2), - CoolantConsumerPosition = new(3, 3), - ElectricityConsumerPosition = new(3, 4) - } - ] + Reactors = [new() { ReactorId = 1, ControlPosition = new(5, 3) }] }; } + private static LevelState DoorLevel(LevelState? seed = null) + { + var level = seed ?? LevelState.Create("Door", 6, 6); + level = level.SetTerrain(new(3, 1), ECellTerrain.Wall); + level = level.SetTerrain(new(3, 3), ECellTerrain.Wall); + return level.SetProp(new(3, 2), new() { Type = EPropType.Door, DoorState = EDoorState.Closed }); + } + private static LevelState AddLine(LevelState level, ECarrierType carrier, GridPosition a, GridPosition b) { level = level.SetUnderground(a, carrier, new() { State = EUndergroundState.Intact }); @@ -435,13 +320,5 @@ public sealed class SimulationEngineTests return level.SetProp(new(2, 3), new() { Type = EPropType.Junction, Carrier = ECarrierType.Fuel, JunctionMode = mode }); } - private static LevelState AddHorizontalUnderground(LevelState level, ECarrierType carrier, int startX, int endX, int y) - { - for (var x = startX; x <= endX; x++) - level = level.SetUnderground(new(x, y), carrier, new() { State = EUndergroundState.Intact }); - - return level; - } - private readonly SimulationEngine m_Engine = new(); } \ No newline at end of file