Add pulse contract and isolation valves

This commit is contained in:
2026-05-14 10:11:35 +02:00
parent c5688d2c0d
commit 6db3e60fd1
15 changed files with 195 additions and 27 deletions

View File

@@ -40,9 +40,10 @@ public partial class InventoryStrip : HBoxContainer
return item;
}
private Label m_WaterLabel = null!;
private Label m_ElectricLabel = null!;
private Label m_FuelLabel = null!;
private Label m_HeatLabel = null!;
private Label m_WaterLabel = null!;
}

View File

@@ -121,6 +121,7 @@ public abstract class Balancing
public abstract int DefaultLevelHeight { get; }
public abstract int MinimumLevelSize { get; }
public abstract int ForecastHorizon { get; }
public abstract int StepsPerPulse { get; }
public abstract float MinValue { get; }
public abstract float MaxValue { get; }
public abstract float FuelSafe { get; }

View File

@@ -6,6 +6,7 @@ public class NormalBalancing : Balancing
public override int DefaultLevelHeight => 12;
public override int MinimumLevelSize => 4;
public override int ForecastHorizon => 6;
public override int StepsPerPulse => 3;
public override float MinValue => 0;
public override float MaxValue => 10;
public override float FuelSafe => 1.5f;

View File

@@ -7,6 +7,7 @@ public enum EEditorTool
Wall,
Underground,
Flow,
IsolationValve,
Consumer,
Junction,
Door,

View File

@@ -139,6 +139,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.IsolationValve => SetCarrierProp(level, position, EPropType.IsolationValve, 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 => ToggleOrSetDoor(level, position),

View File

@@ -11,6 +11,7 @@ public sealed class LevelValidator
ValidateRobot(level, errors);
ValidateCells(level, errors);
ValidateDoors(level, errors);
ValidateIsolationValves(level, errors);
ValidateLeaks(level, errors);
ValidateReactors(level, errors, warnings);
ValidateJunctions(level, errors);
@@ -19,6 +20,25 @@ public sealed class LevelValidator
return new() { Errors = errors, Warnings = warnings };
}
private static void ValidateIsolationValves(LevelState level, List<ValidationIssue> errors)
{
foreach (var position in LevelTraversal.AllPositions(level))
{
var prop = level.GetProp(position);
if (prop.Type != EPropType.IsolationValve)
continue;
if (!level.IsFloor(position))
{
errors.Add(new("Isolation valve must be placed on a floor cell.", position));
continue;
}
if (!level.GetUnderground(position, prop.Carrier).IsPresent)
errors.Add(new("Isolation valve must sit on its matching underground carrier.", position));
}
}
private static void ValidateDimensions(LevelState level, List<ValidationIssue> errors)
{
if (level.Width < Balancing.Current.MinimumLevelSize || level.Height < Balancing.Current.MinimumLevelSize)

View File

@@ -4,6 +4,7 @@ public enum EPropType
{
None,
Flow,
IsolationValve,
Consumer,
Junction,
Door,

View File

@@ -3,6 +3,8 @@
public sealed record GlobalState
{
public int Turn { get; init; }
public int Pulse { get; init; }
public int Step { get; init; }
public ELevelState LevelState { get; init; } = ELevelState.Stable;
public string Status { get; init; } = "STABLE";
public bool TerminalLoss { get; init; }

View File

@@ -26,4 +26,5 @@ public sealed record PropState
public EDoorState DoorState { get; init; } = EDoorState.Closed;
public bool IsEnabled => SwitchState == EPropSwitchState.Enabled;
public bool IsOpen => SwitchState == EPropSwitchState.Enabled;
}

View File

@@ -9,22 +9,17 @@ public sealed class SimulationEngine
public LevelState InteractProp(LevelState level)
{
return PlayerActionSystem.InteractProp(level, ResolveStep);
return PlayerActionSystem.InteractProp(level, ResolvePulse);
}
public LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy)
{
return PlayerActionSystem.InteractLeak(level, carrier, useRemedy, ResolveStep);
return PlayerActionSystem.InteractLeak(level, carrier, useRemedy, ResolvePulse);
}
public LevelState ApplyHeatShield(LevelState level)
{
return PlayerActionSystem.ApplyHeatShield(level, ResolveStep);
}
private LevelState ResolveStep(LevelState level)
{
return ResolveStep(level, true);
return PlayerActionSystem.ApplyHeatShield(level, ResolvePulse);
}
public LevelState ActivateReactor(LevelState level)
@@ -32,14 +27,26 @@ public sealed class SimulationEngine
return ReactorSystem.Activate(level);
}
public LevelState EndTurn(LevelState level)
public LevelState AdvancePulseForDebug(LevelState level)
{
return ResolvePulse(level);
}
public LevelState AdvanceStepForDebug(LevelState level)
{
return ResolveStep(level);
}
[Obsolete("Use AdvancePulseForDebug. Player-facing wait/turn advancement is not part of normal gameplay.")]
public LevelState EndTurn(LevelState level)
{
return AdvancePulseForDebug(level);
}
[Obsolete("Use AdvancePulseForDebug. Player-facing wait/turn advancement is not part of normal gameplay.")]
public LevelState AdvanceTurn(LevelState level)
{
return ResolveStep(level);
return AdvancePulseForDebug(level);
}
public IReadOnlyList<Forecast> Forecast(LevelState level)
@@ -47,7 +54,41 @@ public sealed class SimulationEngine
return ForecastSystem.Forecast(level, simulated => ResolveStep(simulated, false));
}
private LevelState ResolveStep(LevelState level, bool refreshForecasts)
private LevelState ResolvePulse(LevelState level)
{
var next = ValidateAndPropagate(level);
if (next.Global.LevelState == ELevelState.Lost)
return next;
for (var i = 0; i < Balancing.Current.StepsPerPulse; i++)
next = ResolveStepContent(next);
next = ReactorSystem.DeriveState(next);
next = SurfaceInteractionSystem.AdvanceDurations(next);
next = next with {
Global = next.Global with {
Turn = next.Global.Pulse + 1,
Pulse = next.Global.Pulse + 1
}
};
return next with { Forecasts = Forecast(next) };
}
private LevelState ResolveStep(LevelState level, bool refreshForecasts = true)
{
var next = ValidateAndPropagate(level);
if (next.Global.LevelState == ELevelState.Lost)
return next;
next = ResolveStepContent(next);
next = ReactorSystem.DeriveState(next);
next = SurfaceInteractionSystem.AdvanceDurations(next);
return refreshForecasts ? next with { Forecasts = Forecast(next) } : next;
}
private LevelState ValidateAndPropagate(LevelState level)
{
var report = m_Validator.Validate(level);
if (!report.IsValid)
@@ -57,18 +98,16 @@ public sealed class SimulationEngine
next = NetworkPropagationSystem.Propagate(next);
next = ConsumerSystem.Resolve(next);
next = StructuralIntegritySystem.Resolve(next);
return next;
}
private static LevelState ResolveStepContent(LevelState level)
{
var next = level;
next = LeakSystem.Inject(next);
next = SurfaceInteractionSystem.Resolve(next);
next = RobotSafetySystem.Resolve(next);
next = ReactorSystem.DeriveState(next);
next = SurfaceInteractionSystem.AdvanceDurations(next);
next = next with {
Global = next.Global with {
Turn = next.Global.Turn + 1
}
};
return refreshForecasts ? next with { Forecasts = Forecast(next) } : next;
next = next with { Global = next.Global with { Step = next.Global.Step + 1 } };
return next;
}
private readonly LevelValidator m_Validator = new();

View File

@@ -25,4 +25,16 @@ public static class SurfaceStateExtensions
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
};
}
public static bool IsUnsafe(this SurfaceState surface)
{
return surface.Heat >= Balancing.Current.RobotHeatSafetyThreshold
|| surface.Electricity >= Balancing.Current.RobotElectricitySafetyThreshold
|| surface.IsWetElectricUnsafe();
}
public static bool IsWetElectricUnsafe(this SurfaceState surface)
{
return surface.Water > Balancing.Current.WaterSafe && surface.Electricity > Balancing.Current.ElectricitySafe;
}
}

View File

@@ -60,6 +60,9 @@ internal static class NetworkPropagationSystem
if (!level.GetUnderground(next, carrier).CarriesFlow)
continue;
if (IsClosedValveBoundary(level, current.Position, next, carrier))
continue;
var weights = BranchWeights(current.Position, next, junctions);
var amountFactor = current.AmountFactor * weights.Amount;
var intensityFactor = current.IntensityFactor * weights.Intensity;
@@ -75,6 +78,17 @@ internal static class NetworkPropagationSystem
}
}
private static bool IsClosedValveBoundary(LevelState level, GridPosition from, GridPosition to, ECarrierType carrier)
{
return IsClosedValve(level, from, carrier) || IsClosedValve(level, to, carrier);
}
private static bool IsClosedValve(LevelState level, GridPosition position, ECarrierType carrier)
{
return level.GetProp(position) is { Type: EPropType.IsolationValve, Carrier: var valveCarrier, SwitchState: EPropSwitchState.Disabled }
&& valveCarrier == carrier;
}
private static (float Amount, float Intensity) BranchWeights(GridPosition from, GridPosition to, IReadOnlyDictionary<GridPosition, JunctionFlow> junctions)
{
if (!junctions.TryGetValue(from, out var junction))

View File

@@ -7,12 +7,14 @@ internal static class PlayerActionSystem
if (!CanAct(level) || !level.IsFloor(destination) || level.Robot.Position.ManhattanDistance(destination) != 1)
return Refuse(level, "MOVE BLOCKED");
return level with {
var next = level with {
Robot = level.Robot with {
Position = destination,
HeatImmunitySteps = Math.Max(0, level.Robot.HeatImmunitySteps - 1)
}
};
return RobotSafetySystem.ResolveEntry(next);
}
public static LevelState InteractProp(LevelState level, Func<LevelState, LevelState> resolveLengthyAction)
@@ -26,7 +28,7 @@ internal static class PlayerActionSystem
return Refuse(level, "NO PROP");
var next = prop.Type switch {
EPropType.Flow or EPropType.Consumer => ToggleProp(level, position, prop),
EPropType.Flow or EPropType.Consumer or EPropType.IsolationValve => ToggleProp(level, position, prop),
EPropType.Junction => CycleJunctionMode(level, position, prop),
EPropType.Door => ToggleDoor(level, position, prop),
EPropType.AllSeeingEyeTerminal => level with { Global = level.Global with { Status = "ALL-SEEING-EYE AVAILABLE" } },

View File

@@ -3,12 +3,17 @@
internal static class RobotSafetySystem
{
public static LevelState Resolve(LevelState level)
{
return level;
}
public static LevelState ResolveEntry(LevelState level)
{
var surface = level.GetSurface(level.Robot.Position);
var unsafeElement = surface.Fuel >= Balancing.Current.RobotFuelSafetyThreshold || surface.Water >= Balancing.Current.RobotWaterSafetyThreshold || surface.Electricity >= Balancing.Current.RobotElectricitySafetyThreshold;
var unsafeElement = surface.Electricity >= Balancing.Current.RobotElectricitySafetyThreshold || surface.IsWetElectricUnsafe();
var unsafeHeat = surface.Heat >= Balancing.Current.RobotHeatSafetyThreshold && level.Robot.HeatImmunitySteps <= 0;
return unsafeElement || unsafeHeat
? level with { Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = "ROBOT LOST" } }
? level with { Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = "UNSAFE ENTRY LOSS" } }
: level;
}
}