From 9cd9defc0b9aefe79bca1578244a8fe8b70163a8 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Sun, 10 May 2026 18:05:32 +0200 Subject: [PATCH] Unify junction props --- .../Balancing.cs | 22 +++++++++++++- .../Difficulties/NormalBalancing.cs | 15 +++++++++- .../JunctionFlow.cs | 29 ++----------------- .../JunctionFlowAnalyzer.cs | 9 +++--- .../JunctionRatioPreset.cs | 3 ++ .../LevelEditor.cs | 8 ++--- .../Models/ECrossJunctionMode.cs | 9 ------ .../Models/EPropType.cs | 3 +- .../Models/ETJunctionMode.cs | 10 ------- .../Models/PropState.cs | 3 +- .../SimulationEngine.cs | 18 ++++++------ .../MainWindow.xaml.cs | 7 ++--- .../SimulationEngineTests.cs | 18 ++++++------ 13 files changed, 70 insertions(+), 84 deletions(-) create mode 100644 src/ReactorMaintenance.Simulation/JunctionRatioPreset.cs delete mode 100644 src/ReactorMaintenance.Simulation/Models/ECrossJunctionMode.cs delete mode 100644 src/ReactorMaintenance.Simulation/Models/ETJunctionMode.cs diff --git a/src/ReactorMaintenance.Simulation/Balancing.cs b/src/ReactorMaintenance.Simulation/Balancing.cs index cf0590c..d4c6536 100644 --- a/src/ReactorMaintenance.Simulation/Balancing.cs +++ b/src/ReactorMaintenance.Simulation/Balancing.cs @@ -19,6 +19,24 @@ public abstract class Balancing return value >= caution ? EBand.Caution : EBand.Safe; } + public IReadOnlyList JunctionRatios(int outflowCount) + { + return outflowCount switch { + 2 => TwoOutflowJunctionRatios, + 3 => ThreeOutflowJunctionRatios, + _ => Array.Empty() + }; + } + + public float[] JunctionWeights(int outflowCount, int mode) + { + var ratios = JunctionRatios(outflowCount); + if (ratios.Count == 0) + return Array.Empty(); + + return ratios[Math.Clamp(mode, 0, ratios.Count - 1)].Weights; + } + public abstract int DefaultLevelWidth { get; } public abstract int DefaultLevelHeight { get; } public abstract int MinimumLevelSize { get; } @@ -47,6 +65,8 @@ public abstract class Balancing public abstract float SourceIntensity { get; } public abstract float DistanceAmountFalloff { get; } public abstract float DistanceIntensityFalloff { get; } + public abstract IReadOnlyList TwoOutflowJunctionRatios { get; } + public abstract IReadOnlyList ThreeOutflowJunctionRatios { get; } public abstract float ConsumerRequiredAmount { get; } public abstract float ConsumerRequiredIntensity { get; } public abstract float LeakBaseAmount { get; } @@ -69,4 +89,4 @@ public abstract class Balancing public abstract int RemedyBlockTurns { get; } public abstract int HeatShieldSteps { get; } public abstract int InventoryCapacityPerRemedy { get; } -} \ No newline at end of file +} diff --git a/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs b/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs index 9c17301..4338e8f 100644 --- a/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs +++ b/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs @@ -30,6 +30,19 @@ public class NormalBalancing : Balancing public override float SourceIntensity => 8; public override float DistanceAmountFalloff => 0.5f; public override float DistanceIntensityFalloff => 0.4f; + public override IReadOnlyList TwoOutflowJunctionRatios { get; } = [ + new("0/4", [0, 1]), + new("1/3", [0.25f, 0.75f]), + new("2/2", [0.5f, 0.5f]), + new("3/1", [0.75f, 0.25f]), + new("4/0", [1, 0]) + ]; + public override IReadOnlyList ThreeOutflowJunctionRatios { get; } = [ + new("0/3/3", [0, 0.5f, 0.5f]), + new("3/0/3", [0.5f, 0, 0.5f]), + new("3/3/0", [0.5f, 0.5f, 0]), + new("2/2/2", [1f / 3f, 1f / 3f, 1f / 3f]) + ]; public override float ConsumerRequiredAmount => 2.5f; public override float ConsumerRequiredIntensity => 2.5f; public override float LeakBaseAmount => 0.5f; @@ -52,4 +65,4 @@ public class NormalBalancing : Balancing public override int RemedyBlockTurns => 2; public override int HeatShieldSteps => 3; public override int InventoryCapacityPerRemedy => 3; -} \ No newline at end of file +} diff --git a/src/ReactorMaintenance.Simulation/JunctionFlow.cs b/src/ReactorMaintenance.Simulation/JunctionFlow.cs index 5026c2c..520baaa 100644 --- a/src/ReactorMaintenance.Simulation/JunctionFlow.cs +++ b/src/ReactorMaintenance.Simulation/JunctionFlow.cs @@ -17,9 +17,7 @@ public sealed record JunctionFlow if (index < 0) return 0; - var weights = Prop.Type == EPropType.TJunction - ? TJunctionWeights(Prop.TJunctionMode) - : CrossJunctionWeights(Prop.CrossJunctionMode); + var weights = Balancing.Current.JunctionWeights(OutgoingBranches.Count, Prop.JunctionMode); return index < weights.Length ? weights[index] : 0; } @@ -33,27 +31,4 @@ public sealed record JunctionFlow return -1; } - - private static float[] TJunctionWeights(ETJunctionMode mode) - { - return mode switch { - ETJunctionMode.ZeroFour => [0, 1], - ETJunctionMode.OneThree => [0.25f, 0.75f], - ETJunctionMode.TwoTwo => [0.5f, 0.5f], - ETJunctionMode.ThreeOne => [0.75f, 0.25f], - ETJunctionMode.FourZero => [1, 0], - _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported T-junction mode.") - }; - } - - private static float[] CrossJunctionWeights(ECrossJunctionMode mode) - { - return mode switch { - ECrossJunctionMode.ZeroThreeThree => [0, 0.5f, 0.5f], - ECrossJunctionMode.ThreeZeroThree => [0.5f, 0, 0.5f], - ECrossJunctionMode.ThreeThreeZero => [0.5f, 0.5f, 0], - ECrossJunctionMode.TwoTwoTwo => [1f / 3f, 1f / 3f, 1f / 3f], - _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported cross-junction mode.") - }; - } -} \ No newline at end of file +} diff --git a/src/ReactorMaintenance.Simulation/JunctionFlowAnalyzer.cs b/src/ReactorMaintenance.Simulation/JunctionFlowAnalyzer.cs index f57c0d8..c2f9d9a 100644 --- a/src/ReactorMaintenance.Simulation/JunctionFlowAnalyzer.cs +++ b/src/ReactorMaintenance.Simulation/JunctionFlowAnalyzer.cs @@ -12,7 +12,7 @@ public static class JunctionFlowAnalyzer { var position = new GridPosition(x, y); var prop = level.GetProp(position); - if (prop.Type is not (EPropType.TJunction or EPropType.CrossJunction)) + if (prop.Type != EPropType.Junction) continue; flows.Add(AnalyzeJunction(level, position, prop)); @@ -34,9 +34,8 @@ public static class JunctionFlowAnalyzer ? position.Neighbors().Where(level.InBounds).Where(neighbor => level.GetUnderground(neighbor, carrier).CarriesFlow).ToArray() : Array.Empty(); - var expectedBranches = prop.Type == EPropType.TJunction ? 3 : 4; - if (carriers.Length == 1 && branches.Length != expectedBranches) - errors.Add($"{prop.Type} must have exactly {expectedBranches} connected underground branches."); + if (carriers.Length == 1 && branches.Length is not 3 and not 4) + errors.Add("Junction must have one incoming branch and two or three outgoing branches."); var sourceBranches = carriers.Length == 1 ? branches.Select(branch => new SourceBranch(branch, ShortestDistanceToSource(level, branch, position, carrier))) @@ -95,4 +94,4 @@ public static class JunctionFlowAnalyzer } private sealed record SourceBranch(GridPosition Position, int? Distance); -} \ No newline at end of file +} diff --git a/src/ReactorMaintenance.Simulation/JunctionRatioPreset.cs b/src/ReactorMaintenance.Simulation/JunctionRatioPreset.cs new file mode 100644 index 0000000..e587339 --- /dev/null +++ b/src/ReactorMaintenance.Simulation/JunctionRatioPreset.cs @@ -0,0 +1,3 @@ +namespace ReactorMaintenance.Simulation; + +public sealed record JunctionRatioPreset(string Label, float[] Weights); diff --git a/src/ReactorMaintenance.Simulation/LevelEditor.cs b/src/ReactorMaintenance.Simulation/LevelEditor.cs index 2a269ea..0f05fb3 100644 --- a/src/ReactorMaintenance.Simulation/LevelEditor.cs +++ b/src/ReactorMaintenance.Simulation/LevelEditor.cs @@ -14,8 +14,7 @@ public enum EEditorTool FuelConsumer, CoolantConsumer, ElectricityConsumer, - TJunction, - CrossJunction, + Junction, Door, AllSeeingEyeTerminal, FuelRemedySupply, @@ -53,8 +52,7 @@ public static class LevelEditor EEditorTool.FuelConsumer => SetCarrierProp(level, position, EPropType.Consumer, ECarrierType.Fuel), EEditorTool.CoolantConsumer => SetCarrierProp(level, position, EPropType.Consumer, ECarrierType.Coolant), EEditorTool.ElectricityConsumer => SetCarrierProp(level, position, EPropType.Consumer, ECarrierType.Electricity), - EEditorTool.TJunction => SetFloorProp(level, position, new() { Type = EPropType.TJunction }), - EEditorTool.CrossJunction => SetFloorProp(level, position, new() { Type = EPropType.CrossJunction }), + EEditorTool.Junction => SetFloorProp(level, position, new() { Type = EPropType.Junction }), EEditorTool.Door => SetDoor(level, position), EEditorTool.AllSeeingEyeTerminal => SetFloorProp(level, position, new() { Type = EPropType.AllSeeingEyeTerminal }), EEditorTool.FuelRemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.FuelNeutralizer }), @@ -162,4 +160,4 @@ public static class LevelEditor ] }; } -} \ No newline at end of file +} diff --git a/src/ReactorMaintenance.Simulation/Models/ECrossJunctionMode.cs b/src/ReactorMaintenance.Simulation/Models/ECrossJunctionMode.cs deleted file mode 100644 index b195df6..0000000 --- a/src/ReactorMaintenance.Simulation/Models/ECrossJunctionMode.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -public enum ECrossJunctionMode -{ - ZeroThreeThree, - ThreeZeroThree, - ThreeThreeZero, - TwoTwoTwo -} diff --git a/src/ReactorMaintenance.Simulation/Models/EPropType.cs b/src/ReactorMaintenance.Simulation/Models/EPropType.cs index 8f33369..6b0d274 100644 --- a/src/ReactorMaintenance.Simulation/Models/EPropType.cs +++ b/src/ReactorMaintenance.Simulation/Models/EPropType.cs @@ -5,8 +5,7 @@ public enum EPropType None, Flow, Consumer, - TJunction, - CrossJunction, + Junction, Door, AllSeeingEyeTerminal, RemedySupply, diff --git a/src/ReactorMaintenance.Simulation/Models/ETJunctionMode.cs b/src/ReactorMaintenance.Simulation/Models/ETJunctionMode.cs deleted file mode 100644 index 5dbe047..0000000 --- a/src/ReactorMaintenance.Simulation/Models/ETJunctionMode.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -public enum ETJunctionMode -{ - ZeroFour, - OneThree, - TwoTwo, - ThreeOne, - FourZero -} diff --git a/src/ReactorMaintenance.Simulation/Models/PropState.cs b/src/ReactorMaintenance.Simulation/Models/PropState.cs index c5f3d42..0ec5507 100644 --- a/src/ReactorMaintenance.Simulation/Models/PropState.cs +++ b/src/ReactorMaintenance.Simulation/Models/PropState.cs @@ -6,8 +6,7 @@ public sealed record PropState public ECarrierType Carrier { get; init; } public EPropSwitchState SwitchState { get; init; } = EPropSwitchState.Enabled; public EConsumerServiceState ServiceState { get; init; } = EConsumerServiceState.Unknown; - public ETJunctionMode TJunctionMode { get; init; } = ETJunctionMode.TwoTwo; - public ECrossJunctionMode CrossJunctionMode { get; init; } = ECrossJunctionMode.TwoTwoTwo; + public int JunctionMode { get; init; } public ERemedyType RemedyType { get; init; } public bool Depleted { get; init; } public int ReactorId { get; init; } diff --git a/src/ReactorMaintenance.Simulation/SimulationEngine.cs b/src/ReactorMaintenance.Simulation/SimulationEngine.cs index ba6668e..9e4f001 100644 --- a/src/ReactorMaintenance.Simulation/SimulationEngine.cs +++ b/src/ReactorMaintenance.Simulation/SimulationEngine.cs @@ -27,8 +27,7 @@ public sealed class SimulationEngine var next = prop.Type switch { EPropType.Flow or EPropType.Consumer => ToggleProp(level, position, prop), - EPropType.TJunction => level.SetProp(position, prop with { TJunctionMode = NextTJunctionMode(prop.TJunctionMode) }), - EPropType.CrossJunction => level.SetProp(position, prop with { CrossJunctionMode = NextCrossJunctionMode(prop.CrossJunctionMode) }), + 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.RemedySupply => PickUpRemedy(level, position, prop), @@ -631,14 +630,15 @@ public sealed class SimulationEngine return level with { Global = level.Global with { Status = message } }; } - private static ETJunctionMode NextTJunctionMode(ETJunctionMode mode) + private static LevelState CycleJunctionMode(LevelState level, GridPosition position, PropState prop) { - return mode == ETJunctionMode.FourZero ? ETJunctionMode.ZeroFour : mode + 1; - } + var flow = JunctionFlowAnalyzer.Analyze(level).FirstOrDefault(junction => junction.Position == position); + var outflowCount = flow?.OutgoingBranches.Count ?? 2; + var ratios = Balancing.Current.JunctionRatios(outflowCount); + if (ratios.Count == 0) + return level; - private static ECrossJunctionMode NextCrossJunctionMode(ECrossJunctionMode mode) - { - return mode == ECrossJunctionMode.TwoTwoTwo ? ECrossJunctionMode.ZeroThreeThree : mode + 1; + return level.SetProp(position, prop with { JunctionMode = (prop.JunctionMode + 1) % ratios.Count }); } private static EBand SurfaceBand(SurfaceState surface, ECarrierType carrier) @@ -746,4 +746,4 @@ public sealed class SimulationEngine } private readonly LevelValidator m_Validator = new(); -} \ No newline at end of file +} diff --git a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs index 20d9688..de00125 100644 --- a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs +++ b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs @@ -613,7 +613,7 @@ public sealed partial class MainWindow return prop.Type switch { EPropType.Flow => CarrierColor(prop.Carrier), EPropType.Consumer => ColorHelper.FromArgb(255, 93, 123, 170), - EPropType.TJunction or EPropType.CrossJunction => ColorHelper.FromArgb(255, 143, 111, 178), + EPropType.Junction => ColorHelper.FromArgb(255, 143, 111, 178), EPropType.Door => ColorHelper.FromArgb(255, 187, 119, 55), EPropType.AllSeeingEyeTerminal => ColorHelper.FromArgb(255, 85, 151, 156), EPropType.RemedySupply => ColorHelper.FromArgb(255, 76, 145, 86), @@ -637,8 +637,7 @@ public sealed partial class MainWindow return prop.Type switch { EPropType.Flow => $"{CarrierShort(prop.Carrier)} SRC", EPropType.Consumer => $"{CarrierShort(prop.Carrier)} CON", - EPropType.TJunction => $"T {prop.TJunctionMode}", - EPropType.CrossJunction => $"X {prop.CrossJunctionMode}", + EPropType.Junction => $"J {prop.JunctionMode}", EPropType.Door => "DOOR", EPropType.AllSeeingEyeTerminal => "EYE", EPropType.RemedySupply => RemedyShort(prop.RemedyType), @@ -718,4 +717,4 @@ public sealed partial class MainWindow private CanvasBitmap? m_RobotSprite; private CanvasBitmap? m_LeakSprite; private CanvasBitmap? m_HeatSprite; -} \ No newline at end of file +} diff --git a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs index 9ba8ec4..e414c18 100644 --- a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs +++ b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs @@ -87,9 +87,9 @@ public sealed class SimulationEngineTests } [Fact] - public void TJunctionRatioSplitsFlowAcrossInferredOutgoingBranches() + public void JunctionRatioSplitsFlowAcrossInferredOutgoingBranches() { - var level = BuildTJunctionLevel(ETJunctionMode.TwoTwo); + var level = BuildJunctionLevel(2); var next = m_Engine.AdvanceTurn(level); @@ -99,9 +99,9 @@ public sealed class SimulationEngineTests } [Fact] - public void TJunctionZeroWeightBranchReceivesNoIntentionalOutflow() + public void JunctionZeroWeightBranchReceivesNoIntentionalOutflow() { - var level = BuildTJunctionLevel(ETJunctionMode.ZeroFour); + var level = BuildJunctionLevel(0); var next = m_Engine.AdvanceTurn(level); @@ -112,7 +112,7 @@ public sealed class SimulationEngineTests [Fact] public void ValidatorRejectsAmbiguousJunctionSourceBranches() { - var level = BuildTJunctionLevel(ETJunctionMode.TwoTwo); + var level = BuildJunctionLevel(2); level = level.SetProp(new(3, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel }); var report = new LevelValidator().Validate(level); @@ -385,15 +385,15 @@ public sealed class SimulationEngineTests return level; } - private static LevelState BuildTJunctionLevel(ETJunctionMode mode) + private static LevelState BuildJunctionLevel(int mode) { - var level = LevelState.Create("T junction", 6, 6); + var level = LevelState.Create("Junction", 6, 6); level = level.SetUnderground(new(1, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact }); level = level.SetUnderground(new(2, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact }); level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Intact }); level = level.SetUnderground(new(3, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact }); level = level.SetProp(new(1, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel }); - return level.SetProp(new(2, 3), new() { Type = EPropType.TJunction, Carrier = ECarrierType.Fuel, TJunctionMode = mode }); + 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) @@ -405,4 +405,4 @@ public sealed class SimulationEngineTests } private readonly SimulationEngine m_Engine = new(); -} \ No newline at end of file +}