Add branch-aware junction flow

This commit is contained in:
2026-05-10 17:29:19 +02:00
parent b232c0319f
commit cb28eee1dd
6 changed files with 251 additions and 54 deletions

View File

@@ -0,0 +1,59 @@
namespace ReactorMaintenance.Simulation;
public sealed record JunctionFlow
{
public GridPosition Position { get; init; } = new(0, 0);
public PropState Prop { get; init; } = new();
public ECarrierType Carrier { get; init; }
public IReadOnlyList<GridPosition> Branches { get; init; } = Array.Empty<GridPosition>();
public GridPosition? IncomingBranch { get; init; }
public IReadOnlyList<GridPosition> OutgoingBranches { get; init; } = Array.Empty<GridPosition>();
public IReadOnlyList<string> Errors { get; init; } = Array.Empty<string>();
public bool IsValid => Errors.Count == 0;
public float WeightFor(GridPosition outgoingBranch)
{
var index = IndexOfOutgoingBranch(outgoingBranch);
if (index < 0)
return 0;
var weights = Prop.Type == EPropType.TJunction
? TJunctionWeights(Prop.TJunctionMode)
: CrossJunctionWeights(Prop.CrossJunctionMode);
return index < weights.Length ? weights[index] : 0;
}
private int IndexOfOutgoingBranch(GridPosition outgoingBranch)
{
for (var i = 0; i < OutgoingBranches.Count; i++)
{
if (OutgoingBranches[i] == outgoingBranch)
return i;
}
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.")
};
}
}

View File

@@ -0,0 +1,98 @@
namespace ReactorMaintenance.Simulation;
public static class JunctionFlowAnalyzer
{
public static IReadOnlyList<JunctionFlow> Analyze(LevelState level)
{
var flows = new List<JunctionFlow>();
for (var y = 0; y < level.Height; y++)
{
for (var x = 0; x < level.Width; x++)
{
var position = new GridPosition(x, y);
var prop = level.GetProp(position);
if (prop.Type is not (EPropType.TJunction or EPropType.CrossJunction))
continue;
flows.Add(AnalyzeJunction(level, position, prop));
}
}
return flows;
}
private static JunctionFlow AnalyzeJunction(LevelState level, GridPosition position, PropState prop)
{
var errors = new List<string>();
var carriers = Enum.GetValues<ECarrierType>().Where(carrier => level.GetUnderground(position, carrier).IsPresent).ToArray();
var carrier = carriers.FirstOrDefault();
if (carriers.Length != 1)
errors.Add("Junction must regulate exactly one underground carrier.");
var branches = carriers.Length == 1
? position.Neighbors().Where(level.InBounds).Where(neighbor => level.GetUnderground(neighbor, carrier).CarriesFlow).ToArray()
: Array.Empty<GridPosition>();
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.");
var sourceBranches = carriers.Length == 1
? branches.Select(branch => new SourceBranch(branch, ShortestDistanceToSource(level, branch, position, carrier)))
.Where(branch => branch.Distance.HasValue)
.ToArray()
: Array.Empty<SourceBranch>();
GridPosition? incomingBranch = null;
if (sourceBranches.Length > 0)
{
var bestDistance = sourceBranches.Min(branch => branch.Distance!.Value);
var bestBranches = sourceBranches.Where(branch => branch.Distance == bestDistance).ToArray();
if (bestBranches.Length != 1 || sourceBranches.Length != 1)
errors.Add("Ambiguous junction flow.");
else
incomingBranch = bestBranches[0].Position;
}
var outgoingBranches = incomingBranch is null
? Array.Empty<GridPosition>()
: branches.Where(branch => branch != incomingBranch).ToArray();
return new() {
Position = position,
Prop = prop,
Carrier = carrier,
Branches = branches,
IncomingBranch = incomingBranch,
OutgoingBranches = outgoingBranches,
Errors = errors
};
}
private static int? ShortestDistanceToSource(LevelState level, GridPosition start, GridPosition blocked, ECarrierType carrier)
{
var visited = new HashSet<GridPosition> { blocked, start };
var open = new Queue<(GridPosition Position, int Distance)>();
open.Enqueue((start, 0));
while (open.Count > 0)
{
var current = open.Dequeue();
if (level.GetProp(current.Position) is { Type: EPropType.Flow, Carrier: var sourceCarrier, SwitchState: EPropSwitchState.Enabled } && sourceCarrier == carrier)
return current.Distance;
foreach (var next in current.Position.Neighbors().Where(level.InBounds))
{
if (!visited.Add(next) || !level.GetUnderground(next, carrier).CarriesFlow)
continue;
open.Enqueue((next, current.Distance + 1));
}
}
return null;
}
private sealed record SourceBranch(GridPosition Position, int? Distance);
}

View File

@@ -1,4 +1,4 @@
namespace ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation;
public sealed class LevelValidator
{
@@ -113,20 +113,8 @@ public sealed class LevelValidator
private static void ValidateJunctions(LevelState level, List<ValidationIssue> errors)
{
for (var y = 0; y < level.Height; y++)
{
for (var x = 0; x < level.Width; x++)
{
var position = new GridPosition(x, y);
var prop = level.GetProp(position);
if (prop.Type is not (EPropType.TJunction or EPropType.CrossJunction))
continue;
var carrierCount = Enum.GetValues<ECarrierType>().Count(carrier => level.GetUnderground(position, carrier).IsPresent);
if (carrierCount != 1)
errors.Add(new("Junction must regulate exactly one underground carrier.", position));
}
}
foreach (var junction in JunctionFlowAnalyzer.Analyze(level))
errors.AddRange(junction.Errors.Select(error => new ValidationIssue(error, junction.Position)));
}
private static void ValidateRuleEvents(LevelState level, List<ValidationIssue> errors)

View File

@@ -1,4 +1,4 @@
namespace ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation;
public sealed class SimulationEngine
{
@@ -156,9 +156,10 @@ public sealed class SimulationEngine
{
var layer = level.Layer(carrier).ToArray();
var sources = AllPositions(level).Where(position => level.GetProp(position) is { Type: EPropType.Flow, SwitchState: EPropSwitchState.Enabled, Carrier: var sourceCarrier } && sourceCarrier == carrier && level.GetUnderground(position, carrier).CarriesFlow).ToArray();
var junctions = JunctionFlowAnalyzer.Analyze(level).Where(junction => junction.IsValid && junction.Carrier == carrier).ToDictionary(junction => junction.Position);
foreach (var source in sources)
ApplySourceFlow(level, layer, source, carrier);
ApplySourceFlow(level, layer, source, carrier, junctions);
return carrier switch {
ECarrierType.Fuel => level with { Fuel = layer },
@@ -168,7 +169,7 @@ public sealed class SimulationEngine
};
}
private static void ApplySourceFlow(LevelState level, UndergroundCell[] layer, GridPosition source, ECarrierType carrier)
private static void ApplySourceFlow(LevelState level, UndergroundCell[] layer, GridPosition source, ECarrierType carrier, IReadOnlyDictionary<GridPosition, JunctionFlow> junctions)
{
var open = new Queue<(GridPosition Position, int Distance, float AmountFactor, float IntensityFactor)>();
var best = new Dictionary<GridPosition, float>();
@@ -191,7 +192,7 @@ public sealed class SimulationEngine
if (!level.GetUnderground(next, carrier).CarriesFlow)
continue;
var weights = BranchWeights(level, current.Position, next);
var weights = BranchWeights(current.Position, next, junctions);
var amountFactor = current.AmountFactor * weights.Amount;
var intensityFactor = current.IntensityFactor * weights.Intensity;
if (amountFactor <= 0 || intensityFactor <= 0)
@@ -206,38 +207,12 @@ public sealed class SimulationEngine
}
}
private static (float Amount, float Intensity) BranchWeights(LevelState level, GridPosition from, GridPosition to)
private static (float Amount, float Intensity) BranchWeights(GridPosition from, GridPosition to, IReadOnlyDictionary<GridPosition, JunctionFlow> junctions)
{
var prop = level.GetProp(from);
return prop.Type switch {
EPropType.TJunction => TJunctionWeights(prop.TJunctionMode),
EPropType.CrossJunction => CrossJunctionWeights(prop.CrossJunctionMode),
_ => (1, 1)
};
}
if (!junctions.TryGetValue(from, out var junction))
return (1, 1);
private static (float Amount, float Intensity) TJunctionWeights(ETJunctionMode mode)
{
var weight = mode switch {
ETJunctionMode.ZeroFour => 0,
ETJunctionMode.OneThree => 0.25f,
ETJunctionMode.TwoTwo => 0.5f,
ETJunctionMode.ThreeOne => 0.75f,
ETJunctionMode.FourZero => 1,
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported T-junction mode.")
};
return (weight, weight);
}
private static (float Amount, float Intensity) CrossJunctionWeights(ECrossJunctionMode mode)
{
var weight = mode switch {
ECrossJunctionMode.ZeroThreeThree => 0,
ECrossJunctionMode.ThreeZeroThree => 0.5f,
ECrossJunctionMode.ThreeThreeZero => 0.5f,
ECrossJunctionMode.TwoTwoTwo => 1f / 3f,
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported cross-junction mode.")
};
var weight = junction.WeightFor(to);
return (weight, weight);
}