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